From 704563255e529180d8d745d24e5b67fbd141bcb2 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Mon, 29 Jun 2026 16:10:38 +0200 Subject: [PATCH 01/54] add deep link auth state --- .../AuthenticationScreenController.cs | 1 + ...entityVerificationDappDeepLinkAuthState.cs | 102 ++++++++++++++++++ ...yVerificationDappDeepLinkAuthState.cs.meta | 3 + .../States/LoginSelectionAuthState.cs | 3 +- 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs create mode 100644 Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs.meta diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs index 22f49b29d4b..334e22b185f 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs @@ -166,6 +166,7 @@ protected override void OnViewInstantiated() new LoginSelectionAuthState(fsm, viewInstance, this, CurrentState, splashScreen, web3Authenticator, webBrowser, enableEmailOTP), new ProfileFetchingAuthState(fsm, viewInstance, this, CurrentState, selfProfile, storedIdentityProvider), new IdentityVerificationDappAuthState(fsm, viewInstance, this, CurrentState, web3Authenticator), + new IdentityVerificationDappDeepLinkAuthState(fsm, viewInstance, this, web3Authenticator), new LobbyForExistingAccountAuthState(fsm, viewInstance, this, splashScreen, CurrentState, characterPreviewController), new LobbyForNewAccountAuthState(fsm, viewInstance, this, CurrentState, characterPreviewController, selfProfile, wearablesProvider, webBrowser, webRequestController, decentralandUrlsSource, profileChangesBus) ); diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs new file mode 100644 index 00000000000..7236bbf53c2 --- /dev/null +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs @@ -0,0 +1,102 @@ +using Cysharp.Threading.Tasks; +using DCL.Diagnostics; +using DCL.Web3; +using DCL.Web3.Authenticators; +using DCL.Web3.Identities; +using MVC; +using Plugins.NativeWindowManager; +using System; +using System.Threading; +using static DCL.UI.UIAnimationHashes; + +namespace DCL.AuthenticationScreenFlow +{ + public class IdentityVerificationDappDeepLinkAuthState : AuthStateBase, IPayloadedState<(LoginMethod method, CancellationToken ct)> + { + private readonly MVCStateMachine machine; + private readonly AuthenticationScreenController controller; + private readonly ICompositeWeb3Provider compositeWeb3Provider; + + private Exception? loginException; + + public IdentityVerificationDappDeepLinkAuthState( + MVCStateMachine machine, + AuthenticationScreenView viewInstance, + AuthenticationScreenController controller, + ICompositeWeb3Provider compositeWeb3Provider) : base(viewInstance) + { + this.machine = machine; + this.controller = controller; + this.compositeWeb3Provider = compositeWeb3Provider; + } + + public void Enter((LoginMethod method, CancellationToken ct) payload) + { + base.Enter(); + + loginException = null; + + // Checks the current screen mode because it could have been overridden with Alt+Enter + NativeWindowManager.RequestTemporaryWindowMode(); + + controller.CurrentRequestID = string.Empty; + + AuthenticateAsync(payload.method, payload.ct).Forget(); + } + + public override void Exit() + { + if (loginException != null) + { + spanErrorInfo = loginException switch + { + OperationCanceledException => new SpanErrorInfo("Login process was cancelled by user"), + SignatureExpiredException ex => new SpanErrorInfo("Web3 signature expired during deep-link authentication", ex), + Web3SignatureException ex => new SpanErrorInfo("Web3 signature validation failed during deep-link authentication", ex), + Web3Exception ex => new SpanErrorInfo("Connection error during deep-link authentication flow", ex), + Exception ex => new SpanErrorInfo("Unexpected error during deep-link authentication flow", ex), + }; + + if (loginException is not OperationCanceledException) + ReportHub.LogException(loginException, new ReportData(ReportCategory.AUTHENTICATION)); + } + + NativeWindowManager.ReleaseTemporaryWindowMode(); + base.Exit(); + } + + private async UniTaskVoid AuthenticateAsync(LoginMethod method, CancellationToken ct) + { + try + { + IWeb3Identity identity = await compositeWeb3Provider.LoginAsync(LoginPayload.ForDappFlow(method), ct); + machine.Enter(new (identity, false, ct)); + } + catch (OperationCanceledException e) + { + loginException = e; + machine.Enter(SLIDE); + } + catch (SignatureExpiredException e) + { + loginException = e; + machine.Enter(SLIDE); + } + catch (Web3SignatureException e) + { + loginException = e; + machine.Enter(SLIDE); + } + catch (Web3Exception e) + { + loginException = e; + machine.Enter(ErrorType.CONNECTION_ERROR); + } + catch (Exception e) + { + loginException = e; + machine.Enter(ErrorType.CONNECTION_ERROR); + } + } + } +} diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs.meta b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs.meta new file mode 100644 index 00000000000..ff88a635015 --- /dev/null +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 751b79780c1444718391d7d388ae0fff +timeCreated: 1766517747 diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs index e403038f4bf..948868c760e 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs @@ -174,7 +174,8 @@ private void Login(LoginMethod method) currentState.Value = AuthStatus.LoginRequested; view.SetLoadingSpinnerVisibility(true); - machine.Enter((method, controller.GetRestartedLoginToken())); + // machine.Enter((method, controller.GetRestartedLoginToken())); + machine.Enter((method, controller.GetRestartedLoginToken())); } private void OTPLogin() From 8d1ff32f27a921001dfebfc77ffc6235f64eff28 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Mon, 29 Jun 2026 16:45:35 +0200 Subject: [PATCH 02/54] dappWeb3 flow --- .../Dapp/DappWeb3Authenticator.Deeplink.cs | 178 ++++++++++++++++++ .../DappWeb3Authenticator.Deeplink.cs.meta | 2 + .../Dapp/DappWeb3Authenticator.cs | 5 +- 3 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs create mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs.meta diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs new file mode 100644 index 00000000000..024feeae984 --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs @@ -0,0 +1,178 @@ +using CommunicationData.URLHelpers; +using Cysharp.Threading.Tasks; +using DCL.Diagnostics; +using DCL.Web3.Abstract; +using DCL.Web3.Chains; +using DCL.Web3.Identities; +using DCL.WebRequests; +using Nethereum.Signer; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; + +namespace DCL.Web3.Authenticators +{ + public partial class DappWeb3Authenticator + { + // The deep-link flow waits for the user to sign in their browser and for the OS to route the resulting + // deep link back to this process; this can take much longer than a socket round-trip. + private const int DEEPLINK_TIMEOUT_SECONDS = 300; + + // Deep-link sign-in collaborators. Not yet wired through the constructor: the production wiring + // (web request controller + signin dispatcher) is the next step. + private readonly IWebRequestController? webRequestController; + private readonly IDeeplinkSigninDispatcher? deeplinkSigninDispatcher; + + /// + /// Identity-based deep-link sign-in. Same setup as (mutex, socket connect, emit + /// "request" to obtain a requestId, open the browser) but the URL carries flow=deeplink and this method + /// does NOT subscribe to the socket "outcome" event. Instead it awaits the + /// for the identityId that the browser delivers via an OS-routed deep link, then resolves the identity + /// via (GET /identities/{id}). + /// + /// The browser owns the ephemeral keypair in this flow: it generates the keypair, builds the full AuthIdentity + /// and POSTs it to /identities, ignoring any ephemeral params Unity sends. The final identity is therefore + /// resolved entirely from the fetcher; the local ephemeral generated below is NOT used to build it. The + /// dcl_personal_sign "request" emit is still required: auth-app's RequestPage needs a requestId in + /// its URL path, and the server only mints that requestId in response to the emit. We keep the emit's + /// message byte-identical to (the proven, server-accepted form). + /// + public async UniTask LoginViaDeeplinkAsync(LoginPayload payload, CancellationToken ct) + { + if (webRequestController == null || deeplinkSigninDispatcher == null) + throw new Web3Exception($"{nameof(LoginViaDeeplinkAsync)} requires the web request controller and the signin dispatcher to be wired (production path only)."); + + await mutex.WaitAsync(ct); + +#if !UNITY_WEBGL + SynchronizationContext originalSyncContext = SynchronizationContext.Current; // IGNORE_LINE_WEBGL_THREAD_SAFETY_FLAG +#endif + + try + { + await UniTask.SwitchToMainThread(ct); + + await ConnectToAuthApiAsync(); + + // Emit "request" exactly like LoginAsync to mint a requestId for the browser URL. The local ephemeral is + // not used to build the final identity (the browser owns the real keypair; we resolve via the fetcher), + // but we still send the standard well-formed message so the server accepts the emit and recover(requestId) + // returns a valid request. + var ephemeralAccount = web3AccountFactory.CreateRandomAccount(); + + DateTime sessionExpiration = identityExpirationDuration != null + ? DateTime.UtcNow.AddSeconds(identityExpirationDuration.Value) + : DateTime.UtcNow.AddDays(IDENTITY_EXPIRATION_PERIOD); + + string ephemeralMessage = CreateEphemeralMessage(ephemeralAccount, sessionExpiration); + + SignatureIdResponse authenticationResponse = await RequestEthMethodWithSignatureAsync(new LoginAuthApiRequest + { + method = "dcl_personal_sign", + @params = new object[] { ephemeralMessage }, + }, ct); + + await UniTask.SwitchToMainThread(ct); + + string url = $"{signatureWebAppUrl}/{authenticationResponse.requestId}?loginMethod={payload.Method}&flow=deeplink"; + + webBrowser.OpenUrl(url); + + // The browser builds and stores the AuthIdentity, then opens decentraland://?signin={identityId}; + // the OS routes it to DeepLinkHandle, which dispatches it here. We do not listen to socket "outcome". + string identityId = await WaitForSigninAsync(ct); + + IWeb3Identity identity = await FetchIdentityByIdAsync(identityId, IWeb3Identity.Web3IdentitySource.Deeplink, ct); + + await DisconnectFromAuthApiAsync(); + + return identity; + } + catch (Exception) + { + await DisconnectFromAuthApiAsync(); + throw; + } + finally + { +#if !UNITY_WEBGL + if (originalSyncContext != null) + await UniTask.SwitchToSynchronizationContext(originalSyncContext, CancellationToken.None); + else + await UniTask.SwitchToMainThread(CancellationToken.None); +#else + await UniTask.SwitchToMainThread(CancellationToken.None); +#endif + + mutex.Release(); + } + } + + /// + /// Awaits the first identityId the dispatcher delivers, applying a timeout and honoring external + /// cancellation. Disposing the subscription on completion clears the dispatcher buffer so a deep link consumed + /// (or cancelled) by one attempt does not bleed into the next. + /// + private async UniTask WaitForSigninAsync(CancellationToken ct) + { + var completionSource = new UniTaskCompletionSource(); + + using IDisposable subscription = deeplinkSigninDispatcher!.Subscribe(identityId => completionSource.TrySetResult(identityId)); + + // Realtime, not the default DeltaTime: the wait spans the user switching to the browser and back, during + // which the app is backgrounded and game time stalls. Only wall-clock measures the deadline correctly. + return await completionSource.Task.Timeout(TimeSpan.FromSeconds(DEEPLINK_TIMEOUT_SECONDS), DelayType.Realtime).AttachExternalCancellation(ct); + } + + /// + /// Fetches a stored identity by its opaque id via GET {authApiUrl}/identities/{id} and reconstructs a + /// fully-formed . + /// + private async UniTask FetchIdentityByIdAsync(string identityId, IWeb3Identity.Web3IdentitySource source, CancellationToken ct) + { + urlBuilder.Clear(); + + urlBuilder.AppendDomain(URLDomain.FromString(authApiUrl)) + .AppendPath(new URLPath($"identities/{identityId}")); + + var commonArguments = new CommonArguments(urlBuilder.Build()); + + IdentityAuthResponseDto json = await webRequestController!.GetAsync(commonArguments, ct, ReportCategory.AUTHENTICATION) + .CreateFromNewtonsoftJsonAsync(); + + var authChain = AuthChain.Create(); + + foreach (AuthLink authLink in json.identity.authChain) + authChain.Set(authLink); + + string address = authChain.Get(AuthLinkType.SIGNER).payload; + IWeb3Account ephemeralAccount = web3AccountFactory.CreateAccount(new EthECKey(json.identity.ephemeralIdentity.privateKey)); + DateTime expiration = DateTime.Parse(json.identity.expiration, null, DateTimeStyles.RoundtripKind); + + return new DecentralandIdentity(new Web3Address(address), ephemeralAccount, expiration, authChain, source); + } + + [Serializable] + private struct IdentityAuthResponseDto + { + public IdentityDto identity; + + [Serializable] + public struct IdentityDto + { + public string expiration; + public EphemeralIdentityDto ephemeralIdentity; + public List authChain; + } + + [Serializable] + public struct EphemeralIdentityDto + { + public string address; + public string privateKey; + public string publicKey; + } + } + } +} diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs.meta new file mode 100644 index 00000000000..8dd48fce069 --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d02f096ad88773b4f9cd09adac95726e \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs index b0e3f6fc317..28ffead46a3 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs @@ -140,6 +140,9 @@ public async UniTask SendAsync(EthApiRequest request, Web3Reques return await SendWithConfirmationAsync(request, ct); } + public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) => + await LoginViaDeeplinkAsync(payload, ct); + /// /// 1. An authentication request is sent to the server /// 2. Open a tab to let the user sign through the browser with his custom installed wallet @@ -147,7 +150,7 @@ public async UniTask SendAsync(EthApiRequest request, Web3Reques /// /// Login payload containing the authentication method /// - public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) + public async UniTask LoginAsync2(LoginPayload payload, CancellationToken ct) { await mutex.WaitAsync(ct); From f3265360768e9e023bd6c608bbdf29b4289b20a8 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Mon, 29 Jun 2026 17:49:42 +0200 Subject: [PATCH 03/54] add deepLink handling --- .../Global/Dynamic/BootstrapContainer.cs | 16 +++++- .../DeeplinkSigninDispatcher.cs | 56 +++++++++++++++++++ .../DeeplinkSigninDispatcher.cs.meta | 2 + .../IDeeplinkSigninDispatcher.cs | 32 +++++++++++ .../IDeeplinkSigninDispatcher.cs.meta | 2 + .../Dapp/DappWeb3Authenticator.Deeplink.cs | 15 ++--- .../Dapp/DappWeb3Authenticator.cs | 6 ++ 7 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs create mode 100644 Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs.meta create mode 100644 Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs create mode 100644 Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs.meta diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index 0d15ff9655d..c6c338fbae6 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -11,6 +11,7 @@ using DCL.PerformanceAndDiagnostics.Analytics; using DCL.PluginSystem; using DCL.PluginSystem.Global; +using DCL.RuntimeDeepLink; using DCL.SceneLoadingScreens.SplashScreen; using DCL.Time; using DCL.Utility; @@ -48,6 +49,13 @@ public class BootstrapContainer : DCLGlobalContainer public IBootstrap? Bootstrap { get; private set; } public IWeb3IdentityCache? IdentityCache { get; private set; } public ICompositeWeb3Provider? CompositeWeb3Provider { get; private set; } + + /// + /// Shared single-slot signin deep-link bus. Created here (the first container, before any consumer) so the + /// SAME instance is injected into both the Dapp authenticator (which awaits it during the deep-link login) + /// and the runtime DeepLinkHandle (which dispatches into it). + /// + public IDeeplinkSigninDispatcher DeeplinkSigninDispatcher { get; private set; } public AnalyticsContainer Analytics { get; private set; } public DebugSettings.DebugSettings DebugSettings { get; private set; } public VolumeBus VolumeBus { get; private set; } @@ -129,7 +137,8 @@ await bootstrapContainer.InitializeContainerAsync(sceneLoaderSettings.Web3ReadOnlyMethods), dclEnvironment, new AuthCodeVerificationFeatureFlag(), + webRequestController, + deeplinkSigninDispatcher, identityExpirationDuration ); diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs new file mode 100644 index 00000000000..faef537bd8c --- /dev/null +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs @@ -0,0 +1,56 @@ +using System; + +namespace DCL.RuntimeDeepLink +{ + /// + public class DeeplinkSigninDispatcher : IDeeplinkSigninDispatcher + { + private string? bufferedIdentityId; + private Subscription? current; + + public IDisposable Subscribe(Action onSigninReceived, string? expectedRequestId = null) + { + var subscription = new Subscription(this, onSigninReceived); + current = subscription; + + if (bufferedIdentityId != null) + onSigninReceived(bufferedIdentityId); + + return subscription; + } + + public void Dispatch(string identityId, string? sourceRequestId = null) + { + // Stage 1: the sourceRequestId / expectedRequestId correlation is intentionally not yet applied. + bufferedIdentityId = identityId; + current?.Handler(identityId); + } + + private void Remove(Subscription subscription) + { + if (current != subscription) + return; + + current = null; + bufferedIdentityId = null; + } + + private sealed class Subscription : IDisposable + { + public readonly Action Handler; + private DeeplinkSigninDispatcher? owner; + + public Subscription(DeeplinkSigninDispatcher owner, Action handler) + { + this.owner = owner; + Handler = handler; + } + + public void Dispose() + { + owner?.Remove(this); + owner = null; + } + } + } +} diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs.meta b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs.meta new file mode 100644 index 00000000000..7fbd3e47dd4 --- /dev/null +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a6c57a40992ea0a4ba7b6eb9185cde36 \ No newline at end of file diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs b/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs new file mode 100644 index 00000000000..aed5b4e1657 --- /dev/null +++ b/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs @@ -0,0 +1,32 @@ +using System; + +namespace DCL.RuntimeDeepLink +{ + /// + /// Single-event pub/sub for signin deep links. Bridges the producer (DeepLinkHandle, which parses + /// the runtime-arriving signin deep link) and the consumer (the deep-link login, which awaits it) by + /// replaying the most recent unconsumed signin to a late subscriber. + /// + public interface IDeeplinkSigninDispatcher + { + /// + /// Subscribes to incoming signin deep links. Replays the most recent unconsumed signin immediately if one + /// is buffered. Dispose the returned handle to stop listening and clear the buffer. + /// + /// Invoked with the identityId carried by the deep link. + /// + /// Forward-compatible correlation key. Unused in Stage 1; in Stage 2 the dispatcher drops any dispatch + /// whose sourceRequestId does not match. + /// + IDisposable Subscribe(Action onSigninReceived, string? expectedRequestId = null); + + /// + /// Delivers a signin deep link to the active subscriber, or buffers it for the next subscriber. + /// + /// The opaque identity id carried by the deep link. + /// + /// Forward-compatible correlation key carried by the deep link. Unused in Stage 1. + /// + void Dispatch(string identityId, string? sourceRequestId = null); + } +} diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs.meta b/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs.meta new file mode 100644 index 00000000000..c1e71a01f0e --- /dev/null +++ b/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a9d69ed0b48bbb349ab013bb7f5ae765 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs index 024feeae984..ac229ed065c 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs @@ -1,6 +1,7 @@ using CommunicationData.URLHelpers; using Cysharp.Threading.Tasks; using DCL.Diagnostics; +using DCL.RuntimeDeepLink; using DCL.Web3.Abstract; using DCL.Web3.Chains; using DCL.Web3.Identities; @@ -81,9 +82,9 @@ public async UniTask LoginViaDeeplinkAsync(LoginPayload payload, // The browser builds and stores the AuthIdentity, then opens decentraland://?signin={identityId}; // the OS routes it to DeepLinkHandle, which dispatches it here. We do not listen to socket "outcome". - string identityId = await WaitForSigninAsync(ct); + string identityId = await WaitForSigninAsync(deeplinkSigninDispatcher, ct); - IWeb3Identity identity = await FetchIdentityByIdAsync(identityId, IWeb3Identity.Web3IdentitySource.Deeplink, ct); + IWeb3Identity identity = await FetchIdentityByIdAsync(webRequestController, identityId, IWeb3Identity.Web3IdentitySource.Dapp, ct); await DisconnectFromAuthApiAsync(); @@ -114,11 +115,11 @@ public async UniTask LoginViaDeeplinkAsync(LoginPayload payload, /// cancellation. Disposing the subscription on completion clears the dispatcher buffer so a deep link consumed /// (or cancelled) by one attempt does not bleed into the next. /// - private async UniTask WaitForSigninAsync(CancellationToken ct) + private async UniTask WaitForSigninAsync(IDeeplinkSigninDispatcher dispatcher, CancellationToken ct) { var completionSource = new UniTaskCompletionSource(); - using IDisposable subscription = deeplinkSigninDispatcher!.Subscribe(identityId => completionSource.TrySetResult(identityId)); + using IDisposable subscription = dispatcher.Subscribe(identityId => completionSource.TrySetResult(identityId)); // Realtime, not the default DeltaTime: the wait spans the user switching to the browser and back, during // which the app is backgrounded and game time stalls. Only wall-clock measures the deadline correctly. @@ -129,7 +130,7 @@ private async UniTask WaitForSigninAsync(CancellationToken ct) /// Fetches a stored identity by its opaque id via GET {authApiUrl}/identities/{id} and reconstructs a /// fully-formed . /// - private async UniTask FetchIdentityByIdAsync(string identityId, IWeb3Identity.Web3IdentitySource source, CancellationToken ct) + private async UniTask FetchIdentityByIdAsync(IWebRequestController requestController, string identityId, IWeb3Identity.Web3IdentitySource source, CancellationToken ct) { urlBuilder.Clear(); @@ -138,8 +139,8 @@ private async UniTask FetchIdentityByIdAsync(string identi var commonArguments = new CommonArguments(urlBuilder.Build()); - IdentityAuthResponseDto json = await webRequestController!.GetAsync(commonArguments, ct, ReportCategory.AUTHENTICATION) - .CreateFromNewtonsoftJsonAsync(); + IdentityAuthResponseDto json = await requestController.GetAsync(commonArguments, ct, ReportCategory.AUTHENTICATION) + .CreateFromNewtonsoftJsonAsync(); var authChain = AuthChain.Create(); diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs index 28ffead46a3..0b0c16877b5 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs @@ -2,9 +2,11 @@ using Cysharp.Threading.Tasks; using DCL.Browser; using DCL.Multiplayer.Connections.DecentralandUrls; +using DCL.RuntimeDeepLink; using DCL.Web3.Abstract; using DCL.Web3.Chains; using DCL.Web3.Identities; +using DCL.WebRequests; using Newtonsoft.Json; using SocketIOClient; using SocketIOClient.Newtonsoft.Json; @@ -65,6 +67,8 @@ public DappWeb3Authenticator(IWebBrowser webBrowser, HashSet whitelistMethods, HashSet readOnlyMethods, DecentralandEnvironment environment, ICodeVerificationFeatureFlag codeVerificationFeatureFlag, + IWebRequestController? webRequestController = null, + IDeeplinkSigninDispatcher? deeplinkSigninDispatcher = null, int? identityExpirationDuration = null) { this.webBrowser = webBrowser; @@ -77,6 +81,8 @@ public DappWeb3Authenticator(IWebBrowser webBrowser, this.readOnlyMethods = readOnlyMethods; this.environment = environment; this.codeVerificationFeatureFlag = codeVerificationFeatureFlag; + this.webRequestController = webRequestController; + this.deeplinkSigninDispatcher = deeplinkSigninDispatcher; this.identityExpirationDuration = identityExpirationDuration; } From b300840c8ae9f94955b75d64b62853db750463c8 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Tue, 30 Jun 2026 13:59:59 +0200 Subject: [PATCH 04/54] handled deep link flow with tester --- .../Global/AppArgs/AppArgsFlags.cs | 6 + .../AppArgs/ApplicationParametersParser.cs | 5 + .../Global/AppArgs/Tests/AppArgsTests.cs | 23 +++ .../Global/Dynamic/DynamicWorldContainer.cs | 4 +- .../Dynamic/_Scratch/DeeplinkBackendTester.cs | 195 ++++++++++++++++++ .../_Scratch/DeeplinkBackendTester.cs.meta | 2 + .../DeepLinkHandleImplementation.cs | 17 +- .../DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 6 + .../DeeplinkSigninDispatcher.cs | 5 + .../Dapp/DappWeb3Authenticator.Deeplink.cs | 4 +- .../DCL/Web3/Identities/IWeb3Identity.cs | 3 +- 11 files changed, 265 insertions(+), 5 deletions(-) create mode 100644 Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs.meta diff --git a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs index b6c9f39a950..5e0964b0088 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs @@ -31,6 +31,12 @@ public static class AppArgsFlags /// public const string COMMUNITY = "community"; + /// + /// The opaque identity id delivered by the auth website's signin deep link (decentraland://?signin={identityId}). + /// Parsed in DeepLinkHandle and dispatched to the deep-link login awaiting it. + /// + public const string SIGNIN = "signin"; + public const string FORCED_EMOTES = "self-force-emotes"; public const string SELF_PREVIEW_EMOTES = "self-preview-emotes"; public const string SELF_PREVIEW_WEARABLES = "self-preview-wearables"; diff --git a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs index 6c3cbdd1361..dcdf4cb989a 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs @@ -90,6 +90,11 @@ public static Dictionary ProcessDeepLinkParameters(string deepLi { var output = new Dictionary(); + // Drop the optional host segment (e.g. "open" in decentraland://open?signin=...) so only the query remains; + // otherwise the host fuses into the first key (e.g. "open?signin"). Legacy host-less links such as + // decentraland://realm=...&position=... are untouched ('=' follows the word, not '?'). + deepLinkString = Regex.Replace(deepLinkString, @"^(decentraland:/+)[A-Za-z][A-Za-z0-9_-]*\?", "$1?"); + // Update deep link so that Uri class allows the host name deepLinkString = Regex.Replace(deepLinkString, @"^decentraland:/+", "https://decentraland.org/?"); diff --git a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/Tests/AppArgsTests.cs b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/Tests/AppArgsTests.cs index cb8072441a5..baab2acbe97 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/Tests/AppArgsTests.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/Tests/AppArgsTests.cs @@ -1,9 +1,32 @@ using NUnit.Framework; +using System.Collections.Generic; namespace Global.AppArgs.Tests { public class AppArgsTest { + [Test] + public void DeepLinkSigninWithHostSegmentParsesSignin() + { + Dictionary output = ApplicationParametersParser.ProcessDeepLinkParameters("decentraland://open?signin=abc-123"); + Assert.AreEqual("abc-123", output.GetValueOrDefault(AppArgsFlags.SIGNIN), $"keys: {string.Join(", ", output.Keys)}"); + } + + [Test] + public void DeepLinkSigninWithoutHostSegmentParsesSignin() + { + Dictionary output = ApplicationParametersParser.ProcessDeepLinkParameters("decentraland://?signin=abc-123"); + Assert.AreEqual("abc-123", output.GetValueOrDefault(AppArgsFlags.SIGNIN), $"keys: {string.Join(", ", output.Keys)}"); + } + + [Test] + public void DeepLinkLegacyHostlessParamsUnaffectedByHostStripping() + { + Dictionary output = ApplicationParametersParser.ProcessDeepLinkParameters("decentraland://realm=http://127.0.0.1:8000&position=100,100"); + Assert.AreEqual("http://127.0.0.1:8000", output.GetValueOrDefault(AppArgsFlags.REALM), $"keys: {string.Join(", ", output.Keys)}"); + Assert.AreEqual("100,100", output.GetValueOrDefault(AppArgsFlags.POSITION), $"keys: {string.Join(", ", output.Keys)}"); + } + [Test] public void DebugArgSuccessWithoutFlagTest() { diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs index 8648f8be5b0..01eab8d5d9e 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -475,7 +475,9 @@ await MapRendererContainer // Local scene development scenes are excluded from deeplink runtime handling logic if (!appArgs.HasFlag(AppArgsFlags.LOCAL_SCENE)) { - var deepLinkHandleImplementation = new DeepLinkHandle(dynamicWorldParams.StartParcel, chatContainer.ChatTeleporter, ct, communitiesDataService); + // Shared with the Dapp authenticator's deep-link login (created in BootstrapContainer): the browser + // delivers the signin deep link here, DeepLinkHandle dispatches it, and DappWeb3Authenticator awaits it. + var deepLinkHandleImplementation = new DeepLinkHandle(dynamicWorldParams.StartParcel, chatContainer.ChatTeleporter, ct, communitiesDataService, bootstrapContainer.DeeplinkSigninDispatcher); deepLinkHandleImplementation.StartListenForDeepLinksAsync(ct).Forget(); } diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs new file mode 100644 index 00000000000..cdd57abb6cf --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs @@ -0,0 +1,195 @@ +// THROWAWAY scratch tool — manual verification of deep-link sign-in backend communication. +// NOT production code. No style/architecture rules apply here. Do not commit / do not move into a prod path. +// Sits in a _Scratch subfolder next to BootstrapContainer.cs (which constructs DappWeb3Authenticator with the +// exact same deps), so it lands in Assembly-CSharp and every type resolves. Wrapped in #if UNITY_EDITOR so +// Unity never compiles it into a player build. Do not commit; delete when done. +// +// HOW TO USE (Play Mode required — socket.io, web requests, OpenUrl): +// 1. Empty scene -> empty GameObject -> attach this component. +// 2. Enter Play Mode (Awake builds the authenticator). +// 3. Right-click the component header (or the gear icon) to get the context menu, then run the steps in order. +// Read the Unity console between steps. +// +// STEPS (context-menu items): +// "1. Connect + Request" -> full LoginViaDeeplinkAsync: socket connect to auth-api + emit "request" +// (mints requestId) + opens the browser with flow=deeplink. Reaching the browser +// proves the socket handshake worked. Then it BLOCKS waiting for an identityId. +// "2. Dispatch identityId" -> paste an identityId into the Inspector field first, then run this to +// dispatcher.Dispatch(it). Replaces OS deep-link routing; unblocks step 1, which +// then does GET /identities/{id} internally (full E2E). +// "3. Fetch identity by id"-> independent REST check: GET /identities/{id} via FetchIdentityByIdAsync directly +// (uses the Inspector identityId field). +// "Cancel current" -> cancels the in-flight operation and resets the token. + +#if UNITY_EDITOR +using CommunicationData.URLHelpers; +using Cysharp.Threading.Tasks; +using DCL.Browser; +using DCL.Browser.DecentralandUrls; +using DCL.Multiplayer.Connections.DecentralandUrls; +using DCL.RuntimeDeepLink; +using DCL.Time; +using DCL.Utility; +using DCL.Web3.Accounts.Factory; +using DCL.Web3.Authenticators; +using DCL.Web3.Identities; +using DCL.WebRequests; +using DCL.WebRequests.Analytics; +using DCL.WebRequests.RequestsHub; +using DCL.DebugUtilities.UIBindings; +using System; +using System.Collections.Generic; +using System.Threading; +using UnityEngine; + +public class DeeplinkBackendTester : MonoBehaviour +{ + private const DecentralandEnvironment ENVIRONMENT = DecentralandEnvironment.Zone; + + [Tooltip("Paste the identityId here before running steps 2 and 3.")] + [SerializeField] private string identityId = ""; + + private DappWeb3Authenticator authenticator; + private DeeplinkSigninDispatcher dispatcher; + private IWebRequestController webRequestController; + private CancellationTokenSource cts; + private string authApiBaseUrl; + + private void Awake() + { + cts = new CancellationTokenSource(); + + var urls = DecentralandUrlsSource.CreateForTest(ENVIRONMENT, ILaunchMode.PLAY); + authApiBaseUrl = urls.Url(DecentralandUrl.ApiAuth); + + dispatcher = new DeeplinkSigninDispatcher(); + + webRequestController = new WebRequestController( + new WebRequestsAnalyticsContainer(), + new MemoryWeb3IdentityCache(), + new RequestHub(urls), + new WebRequestBudget(int.MaxValue, new ElementBinding(int.MaxValue)), + new RealmClock()); + + var webBrowser = new UnityAppWebBrowser(urls); + + authenticator = new DappWeb3Authenticator( + webBrowser, + URLAddress.FromString(urls.Url(DecentralandUrl.ApiAuth)), + URLAddress.FromString(urls.Url(DecentralandUrl.AuthSignatureWebApp)), + URLDomain.FromString(urls.Url(DecentralandUrl.ApiRpc)), + new MemoryWeb3IdentityCache(), // not touched in deeplink path + new Web3AccountFactory(), + new HashSet(), // whitelistMethods — not read in deeplink path + new HashSet(), // readOnlyMethods — not read in deeplink path + ENVIRONMENT, + new NoopCodeVerificationFeatureFlag(), + webRequestController, + dispatcher, + null); // identityExpirationDuration + + Debug.Log($"[DeeplinkBackendTester] Ready. env={ENVIRONMENT}\n" + + $" auth-api (socket): {urls.Url(DecentralandUrl.ApiAuth)}\n" + + $" signature (browser): {urls.Url(DecentralandUrl.AuthSignatureWebApp)}\n" + + $" rpc : {urls.Url(DecentralandUrl.ApiRpc)}"); + } + + private void OnDestroy() + { + cts?.Cancel(); + cts?.Dispose(); + authenticator?.Dispose(); + } + + [ContextMenu("1. Connect + Request (LoginViaDeeplink)")] + private void Step1_Login() + { + if (!EnsurePlayMode()) return; + RunLoginAsync().Forget(); + } + + [ContextMenu("2. Dispatch identityId")] + private void Step2_Dispatch() + { + if (!EnsurePlayMode()) return; + + string id = (identityId ?? "").Trim(); + + if (string.IsNullOrEmpty(id)) + { + Debug.LogWarning("[DeeplinkBackendTester] [2] identityId field is empty."); + return; + } + + Debug.Log($"[DeeplinkBackendTester] [2] dispatcher.Dispatch(\"{id}\")"); + dispatcher.Dispatch(id); + } + + [ContextMenu("3. Fetch identity by id (GET /identities/{id})")] + private void Step3_Fetch() + { + if (!EnsurePlayMode()) return; + RunFetchAsync().Forget(); + } + + [ContextMenu("Cancel current")] + private void CancelCurrent() + { + if (!EnsurePlayMode()) return; + + Debug.Log("[DeeplinkBackendTester] cancelling current operation, resetting token"); + cts.Cancel(); + cts.Dispose(); + cts = new CancellationTokenSource(); + } + + private async UniTaskVoid RunLoginAsync() + { + Debug.Log("[DeeplinkBackendTester] >> [1] LoginViaDeeplinkAsync START (socket connect + emit request + open browser, then waits for dispatch)"); + + try + { + IWeb3Identity identity = await authenticator.LoginViaDeeplinkAsync(LoginPayload.ForDappFlow(LoginMethod.GOOGLE), cts.Token); + Debug.Log($"[DeeplinkBackendTester] << [1] LOGIN OK. address={identity.Address} expiration={identity.Expiration:O} source={identity.Source} isExpired={identity.IsExpired}"); + } + catch (OperationCanceledException) { Debug.Log("[DeeplinkBackendTester] << [1] cancelled"); } + catch (Exception e) { Debug.LogError($"[DeeplinkBackendTester] << [1] EXCEPTION: {e.GetType().Name}: {e.Message}\n{e}"); } + } + + private async UniTaskVoid RunFetchAsync() + { + string id = (identityId ?? "").Trim(); + + if (string.IsNullOrEmpty(id)) + { + Debug.LogWarning("[DeeplinkBackendTester] [3] identityId field is empty."); + return; + } + + Debug.Log($"[DeeplinkBackendTester] >> [3] FetchIdentityByIdAsync(\"{id}\") GET {authApiBaseUrl}/identities/{id}"); + + try + { + IWeb3Identity identity = await authenticator.FetchIdentityByIdAsync( + webRequestController, id, IWeb3Identity.Web3IdentitySource.Deeplink, cts.Token); + + Debug.Log($"[DeeplinkBackendTester] << [3] FETCH OK. address={identity.Address} expiration={identity.Expiration:O} source={identity.Source} isExpired={identity.IsExpired}"); + } + catch (OperationCanceledException) { Debug.Log("[DeeplinkBackendTester] << [3] cancelled"); } + catch (Exception e) { Debug.LogError($"[DeeplinkBackendTester] << [3] EXCEPTION: {e.GetType().Name}: {e.Message}\n{e}"); } + } + + private bool EnsurePlayMode() + { + if (authenticator != null) return true; + + Debug.LogWarning("[DeeplinkBackendTester] Not initialized — enter Play Mode first (Awake builds the authenticator)."); + return false; + } + + private sealed class NoopCodeVerificationFeatureFlag : DappWeb3Authenticator.ICodeVerificationFeatureFlag + { + public bool ShouldWaitForCodeVerificationFromServer => false; + } +} +#endif diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs.meta b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs.meta new file mode 100644 index 00000000000..bdbf888c249 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 89bb6dbd9477c26439c0b32a02b057c7 \ No newline at end of file diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index a9bdc4b78dc..7873d15510f 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -16,19 +16,34 @@ public class DeepLinkHandle : IDeepLinkHandle private readonly ChatTeleporter chatTeleporter; private readonly CancellationToken token; private readonly CommunityDataService communityDataService; + private readonly IDeeplinkSigninDispatcher deeplinkSigninDispatcher; - public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, CancellationToken token, CommunityDataService communityDataService) + public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, CancellationToken token, CommunityDataService communityDataService, IDeeplinkSigninDispatcher deeplinkSigninDispatcher) { this.startParcel = startParcel; this.chatTeleporter = chatTeleporter; this.token = token; this.communityDataService = communityDataService; + this.deeplinkSigninDispatcher = deeplinkSigninDispatcher; } public string Name => "Real Implementation"; public Result HandleDeepLink(DeepLink deeplink) { + // Signin takes precedence over realm/position/community routing and returns before any teleport or + // community notification is triggered. + string? signin = deeplink.ValueOf(AppArgsFlags.SIGNIN); + + Debug.Log($"[DLDBG] HandleDeepLink received: {deeplink} | extracted signin='{signin}'"); + + if (!string.IsNullOrEmpty(signin)) + { + Debug.Log($"[DLDBG] HandleDeepLink dispatching signin='{signin}'"); + deeplinkSigninDispatcher.Dispatch(signin); + return Result.SuccessResult(); + } + Vector2Int? position = PositionFrom(deeplink); URLDomain? realm = RealmFrom(deeplink); string? communityId = CommunityFrom(deeplink); diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index e7f0b9b3627..8816d22e3c7 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -36,6 +36,8 @@ public static class DeepLinkSentinel /// public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandle handle, CancellationToken token) { + Debug.Log($"[DLDBG] Sentinel STARTED by '{handle.Name}'. Watching: {DEEP_LINK_BRIDGE_PATH}"); + while (token.IsCancellationRequested == false) { bool cancelled = await UniTask.Delay(CHECK_IN_PERIOD, cancellationToken: token).SuppressCancellationThrow(); @@ -44,9 +46,13 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl // File.Exists method is lightweight and can be used in this loop if (!File.Exists(DEEP_LINK_BRIDGE_PATH)) continue; + Debug.Log($"[DLDBG] Sentinel: bridge file FOUND at {DEEP_LINK_BRIDGE_PATH}"); + Result contentResult = await File.ReadAllTextAsync(DEEP_LINK_BRIDGE_PATH, token)!.SuppressToResultAsync(ReportCategory.RUNTIME_DEEPLINKS); if (contentResult.Success == false) continue; + Debug.Log($"[DLDBG] Sentinel: raw content = {contentResult.Value}"); + // Notify emitter that file has been consumed File.Delete(DEEP_LINK_BRIDGE_PATH); diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs index faef537bd8c..8c9dcd9989f 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs @@ -1,4 +1,5 @@ using System; +using UnityEngine; namespace DCL.RuntimeDeepLink { @@ -10,6 +11,8 @@ public class DeeplinkSigninDispatcher : IDeeplinkSigninDispatcher public IDisposable Subscribe(Action onSigninReceived, string? expectedRequestId = null) { + Debug.Log($"[DLDBG] Dispatcher.Subscribe (buffered={bufferedIdentityId ?? ""})"); + var subscription = new Subscription(this, onSigninReceived); current = subscription; @@ -21,6 +24,8 @@ public IDisposable Subscribe(Action onSigninReceived, string? expectedRe public void Dispatch(string identityId, string? sourceRequestId = null) { + Debug.Log($"[DLDBG] Dispatcher.Dispatch id='{identityId}' hasSubscriber={current != null}"); + // Stage 1: the sourceRequestId / expectedRequestId correlation is intentionally not yet applied. bufferedIdentityId = identityId; current?.Handler(identityId); diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs index ac229ed065c..23bc97d11c1 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs @@ -84,7 +84,7 @@ public async UniTask LoginViaDeeplinkAsync(LoginPayload payload, // the OS routes it to DeepLinkHandle, which dispatches it here. We do not listen to socket "outcome". string identityId = await WaitForSigninAsync(deeplinkSigninDispatcher, ct); - IWeb3Identity identity = await FetchIdentityByIdAsync(webRequestController, identityId, IWeb3Identity.Web3IdentitySource.Dapp, ct); + IWeb3Identity identity = await FetchIdentityByIdAsync(webRequestController, identityId, IWeb3Identity.Web3IdentitySource.Deeplink, ct); await DisconnectFromAuthApiAsync(); @@ -130,7 +130,7 @@ private async UniTask WaitForSigninAsync(IDeeplinkSigninDispatcher dispa /// Fetches a stored identity by its opaque id via GET {authApiUrl}/identities/{id} and reconstructs a /// fully-formed . /// - private async UniTask FetchIdentityByIdAsync(IWebRequestController requestController, string identityId, IWeb3Identity.Web3IdentitySource source, CancellationToken ct) + public async UniTask FetchIdentityByIdAsync(IWebRequestController requestController, string identityId, IWeb3Identity.Web3IdentitySource source, CancellationToken ct) { urlBuilder.Clear(); diff --git a/Explorer/Assets/DCL/Web3/Identities/IWeb3Identity.cs b/Explorer/Assets/DCL/Web3/Identities/IWeb3Identity.cs index 58d80e65e36..a7ccc4558d7 100644 --- a/Explorer/Assets/DCL/Web3/Identities/IWeb3Identity.cs +++ b/Explorer/Assets/DCL/Web3/Identities/IWeb3Identity.cs @@ -22,7 +22,8 @@ enum Web3IdentitySource Cached, TokenFile, Dapp, - OTP + OTP, + Deeplink } class Random : IWeb3Identity From 6f985dc83d9cdc1f87da6f92484067a33071e238 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Wed, 1 Jul 2026 13:03:01 +0200 Subject: [PATCH 05/54] not consuming signing deepLink --- ...entityVerificationDappDeepLinkAuthState.cs | 1 + .../DCL/RuntimeDeepLink/DeepLinkHandle.cs | 8 ++-- .../DeepLinkHandleImplementation.cs | 31 ++++++++----- .../DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 44 ++++++++++--------- .../DeeplinkSigninDispatcher.cs | 14 +----- .../IDeeplinkSigninDispatcher.cs | 15 ++++--- 6 files changed, 62 insertions(+), 51 deletions(-) diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs index 7236bbf53c2..a6b2d87ff48 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs @@ -70,6 +70,7 @@ private async UniTaskVoid AuthenticateAsync(LoginMethod method, CancellationToke try { IWeb3Identity identity = await compositeWeb3Provider.LoginAsync(LoginPayload.ForDappFlow(method), ct); + viewInstance.LoginSelectionAuthView.Hide(); machine.Enter(new (identity, false, ct)); } catch (OperationCanceledException e) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs index 748b6c31ceb..74dae195dd0 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs @@ -8,8 +8,10 @@ public interface IDeepLinkHandle /// /// Implementations of the method must be exception free. + /// Returns true when the deep link was consumed and its bridge file may be deleted; + /// false to leave the bridge file in place for a later attempt. /// - public Result HandleDeepLink(DeepLink deeplink); + public bool HandleDeepLink(DeepLink deeplink); class Null : IDeepLinkHandle { @@ -19,8 +21,8 @@ private Null() { } public string Name => "Null Implementation"; - public Result HandleDeepLink(DeepLink deeplink) => - Result.SuccessResult(); + public bool HandleDeepLink(DeepLink deeplink) => + true; } } } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index 7873d15510f..9128a60e5ef 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -2,8 +2,8 @@ using Cysharp.Threading.Tasks; using DCL.Chat.Commands; using DCL.Communities; +using DCL.Diagnostics; using DCL.RealmNavigation; -using DCL.Utility.Types; using Global.AppArgs; using System.Threading; using UnityEngine; @@ -29,26 +29,29 @@ public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, Ca public string Name => "Real Implementation"; - public Result HandleDeepLink(DeepLink deeplink) + public bool HandleDeepLink(DeepLink deeplink) { // Signin takes precedence over realm/position/community routing and returns before any teleport or // community notification is triggered. string? signin = deeplink.ValueOf(AppArgsFlags.SIGNIN); - Debug.Log($"[DLDBG] HandleDeepLink received: {deeplink} | extracted signin='{signin}'"); - if (!string.IsNullOrEmpty(signin)) { - Debug.Log($"[DLDBG] HandleDeepLink dispatching signin='{signin}'"); + // Only consume the signin when a login flow is actively awaiting it; otherwise leave the bridge + // file in place so the instance that is logging in can pick it up. + if (!deeplinkSigninDispatcher.HasSubscriber) + return false; + deeplinkSigninDispatcher.Dispatch(signin); - return Result.SuccessResult(); + ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"{Name} dispatched signin deeplink: {deeplink}"); + return true; } Vector2Int? position = PositionFrom(deeplink); URLDomain? realm = RealmFrom(deeplink); string? communityId = CommunityFrom(deeplink); - var result = Result.ErrorResult("no matches"); + var handled = false; if (realm.HasValue) { @@ -57,7 +60,7 @@ public Result HandleDeepLink(DeepLink deeplink) else chatTeleporter.TeleportToRealmAsync(realm.Value.Value, token).Forget(); - result = Result.SuccessResult(); + handled = true; } else if (position.HasValue) { @@ -68,16 +71,22 @@ public Result HandleDeepLink(DeepLink deeplink) else startParcel.Assign(parcel); - result = Result.SuccessResult(); + handled = true; } if (!string.IsNullOrEmpty(communityId)) { communityDataService.ShowCommunityDeepLinkNotification(communityId); - result = Result.SuccessResult(); + handled = true; } - return result; + if (handled) + ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"{Name} successfully handled deeplink: {deeplink}"); + else + ReportHub.LogWarning(ReportCategory.RUNTIME_DEEPLINKS, $"{Name} found no actionable content in deeplink: {deeplink}"); + + // Non-signin deep links are always consumed: nothing awaits them, so leaving the file would re-loop. + return true; } private static URLDomain? RealmFrom(DeepLink deepLink) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index 8816d22e3c7..e9ae2c368c2 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -5,7 +5,6 @@ using System; using System.IO; using System.Threading; -using UnityEngine; namespace DCL.RuntimeDeepLink { @@ -36,8 +35,6 @@ public static class DeepLinkSentinel /// public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandle handle, CancellationToken token) { - Debug.Log($"[DLDBG] Sentinel STARTED by '{handle.Name}'. Watching: {DEEP_LINK_BRIDGE_PATH}"); - while (token.IsCancellationRequested == false) { bool cancelled = await UniTask.Delay(CHECK_IN_PERIOD, cancellationToken: token).SuppressCancellationThrow(); @@ -46,30 +43,37 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl // File.Exists method is lightweight and can be used in this loop if (!File.Exists(DEEP_LINK_BRIDGE_PATH)) continue; - Debug.Log($"[DLDBG] Sentinel: bridge file FOUND at {DEEP_LINK_BRIDGE_PATH}"); - Result contentResult = await File.ReadAllTextAsync(DEEP_LINK_BRIDGE_PATH, token)!.SuppressToResultAsync(ReportCategory.RUNTIME_DEEPLINKS); - if (contentResult.Success == false) continue; - Debug.Log($"[DLDBG] Sentinel: raw content = {contentResult.Value}"); - - // Notify emitter that file has been consumed - File.Delete(DEEP_LINK_BRIDGE_PATH); + // Transient IO read failure: leave the file for the next check-in. + if (contentResult.Success == false) continue; + // Parse before deleting: a corrupt file is dropped, a valid one is handled. Result deepLinkCreateResult = DeepLink.FromJson(contentResult.Value); - if (deepLinkCreateResult.Success) + if (deepLinkCreateResult.Success == false) { - DeepLink deeplink = deepLinkCreateResult.Value; - Result handleResult = handle.HandleDeepLink(deeplink); - - if (handleResult.Success) - ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"{handle.Name} successfully handled deeplink: {deeplink}"); - else - ReportHub.LogError(ReportCategory.RUNTIME_DEEPLINKS, $"{handle.Name} raised error on handle deeplink: {deeplink}, error {handleResult.ErrorMessage}"); - } - else ReportHub.LogError(ReportCategory.RUNTIME_DEEPLINKS, $"Cannot deserialize deeplink content: {deepLinkCreateResult.ErrorMessage}"); + TryDeleteBridgeFile(); + continue; + } + + // A false result means no login flow is awaiting the signin yet: keep the file so it can be picked up later. + if (handle.HandleDeepLink(deepLinkCreateResult.Value)) + TryDeleteBridgeFile(); + } + } + + private static void TryDeleteBridgeFile() + { + try + { + File.Delete(DEEP_LINK_BRIDGE_PATH); + } + catch (Exception e) + { + // Deletion can fail transiently (file locked/rewritten by the launcher). Log and keep the loop alive. + ReportHub.LogError(ReportCategory.RUNTIME_DEEPLINKS, $"Failed to delete deeplink bridge file: {e.Message}"); } } } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs index 8c9dcd9989f..90252365444 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs @@ -1,33 +1,24 @@ using System; -using UnityEngine; namespace DCL.RuntimeDeepLink { /// public class DeeplinkSigninDispatcher : IDeeplinkSigninDispatcher { - private string? bufferedIdentityId; private Subscription? current; + public bool HasSubscriber => current != null; + public IDisposable Subscribe(Action onSigninReceived, string? expectedRequestId = null) { - Debug.Log($"[DLDBG] Dispatcher.Subscribe (buffered={bufferedIdentityId ?? ""})"); - var subscription = new Subscription(this, onSigninReceived); current = subscription; - - if (bufferedIdentityId != null) - onSigninReceived(bufferedIdentityId); - return subscription; } public void Dispatch(string identityId, string? sourceRequestId = null) { - Debug.Log($"[DLDBG] Dispatcher.Dispatch id='{identityId}' hasSubscriber={current != null}"); - // Stage 1: the sourceRequestId / expectedRequestId correlation is intentionally not yet applied. - bufferedIdentityId = identityId; current?.Handler(identityId); } @@ -37,7 +28,6 @@ private void Remove(Subscription subscription) return; current = null; - bufferedIdentityId = null; } private sealed class Subscription : IDisposable diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs b/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs index aed5b4e1657..665567dc689 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs @@ -4,14 +4,18 @@ namespace DCL.RuntimeDeepLink { /// /// Single-event pub/sub for signin deep links. Bridges the producer (DeepLinkHandle, which parses - /// the runtime-arriving signin deep link) and the consumer (the deep-link login, which awaits it) by - /// replaying the most recent unconsumed signin to a late subscriber. + /// the runtime-arriving signin deep link) and the consumer (the deep-link login, which awaits it). Only a + /// live subscriber receives a signin; there is no buffering, so a signin dispatched with no subscriber is dropped. /// public interface IDeeplinkSigninDispatcher { /// - /// Subscribes to incoming signin deep links. Replays the most recent unconsumed signin immediately if one - /// is buffered. Dispose the returned handle to stop listening and clear the buffer. + /// Whether a live subscriber is currently listening for signin deep links. + /// + bool HasSubscriber { get; } + + /// + /// Subscribes to incoming signin deep links. Dispose the returned handle to stop listening. /// /// Invoked with the identityId carried by the deep link. /// @@ -21,7 +25,8 @@ public interface IDeeplinkSigninDispatcher IDisposable Subscribe(Action onSigninReceived, string? expectedRequestId = null); /// - /// Delivers a signin deep link to the active subscriber, or buffers it for the next subscriber. + /// Delivers a signin deep link to the active subscriber, if any. Dispatched with no subscriber, the signin + /// is dropped; callers gate on before dispatching. /// /// The opaque identity id carried by the deep link. /// From 7f5237d772b9575b9ee69af660cef2d424fef78d Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Wed, 1 Jul 2026 14:55:13 +0200 Subject: [PATCH 06/54] not deleting the file --- .../DCL/RuntimeDeepLink/DeepLinkHandle.cs | 7 ---- .../DeepLinkHandleImplementation.cs | 6 ++++ .../DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 35 +++++++++++++++---- .../DeeplinkSigninDispatcher.cs | 5 +++ 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs index 74dae195dd0..3af698eebcd 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs @@ -1,13 +1,8 @@ -using DCL.Utility.Types; - namespace DCL.RuntimeDeepLink { public interface IDeepLinkHandle { - public string Name { get; } - /// - /// Implementations of the method must be exception free. /// Returns true when the deep link was consumed and its bridge file may be deleted; /// false to leave the bridge file in place for a later attempt. /// @@ -19,8 +14,6 @@ class Null : IDeepLinkHandle private Null() { } - public string Name => "Null Implementation"; - public bool HandleDeepLink(DeepLink deeplink) => true; } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index 9128a60e5ef..fd0f17bbe1a 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -35,13 +35,19 @@ public bool HandleDeepLink(DeepLink deeplink) // community notification is triggered. string? signin = deeplink.ValueOf(AppArgsFlags.SIGNIN); + Debug.Log($"[DLDBG] HandleDeepLink received: {deeplink} | extracted signin='{signin}'"); // TODO: temporary deep-link debug log, remove. + if (!string.IsNullOrEmpty(signin)) { // Only consume the signin when a login flow is actively awaiting it; otherwise leave the bridge // file in place so the instance that is logging in can pick it up. if (!deeplinkSigninDispatcher.HasSubscriber) + { + Debug.Log("[DLDBG] HandleDeepLink deferring signin: no subscriber, leaving bridge file"); // TODO: temporary deep-link debug log, remove. return false; + } + Debug.Log($"[DLDBG] HandleDeepLink dispatching signin='{signin}'"); // TODO: temporary deep-link debug log, remove. deeplinkSigninDispatcher.Dispatch(signin); ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"{Name} dispatched signin deeplink: {deeplink}"); return true; diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index e9ae2c368c2..dcebda3e61e 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Threading; +using UnityEngine; namespace DCL.RuntimeDeepLink { @@ -35,6 +36,8 @@ public static class DeepLinkSentinel /// public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandle handle, CancellationToken token) { + Debug.Log($"[DLDBG] Sentinel STARTED . Watching: {DEEP_LINK_BRIDGE_PATH}"); // TODO: temporary deep-link debug log, remove. + while (token.IsCancellationRequested == false) { bool cancelled = await UniTask.Delay(CHECK_IN_PERIOD, cancellationToken: token).SuppressCancellationThrow(); @@ -43,37 +46,57 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl // File.Exists method is lightweight and can be used in this loop if (!File.Exists(DEEP_LINK_BRIDGE_PATH)) continue; + // TODO: temporary deep-link debug log, remove. FileInfo.Exists is checked to avoid throwing if the + // file vanished between the check above and reading its metadata (launcher rewrite / other instance). + var bridgeInfo = new FileInfo(DEEP_LINK_BRIDGE_PATH); + if (bridgeInfo.Exists) + Debug.Log($"[DLDBG] Sentinel: bridge file FOUND | created={bridgeInfo.CreationTimeUtc:HH:mm:ss.fff}Z written={bridgeInfo.LastWriteTimeUtc:HH:mm:ss.fff}Z size={bridgeInfo.Length}B path={DEEP_LINK_BRIDGE_PATH}"); + Result contentResult = await File.ReadAllTextAsync(DEEP_LINK_BRIDGE_PATH, token)!.SuppressToResultAsync(ReportCategory.RUNTIME_DEEPLINKS); // Transient IO read failure: leave the file for the next check-in. if (contentResult.Success == false) continue; + Debug.Log($"[DLDBG] Sentinel: raw content = {contentResult.Value}"); // TODO: temporary deep-link debug log, remove. + // Parse before deleting: a corrupt file is dropped, a valid one is handled. Result deepLinkCreateResult = DeepLink.FromJson(contentResult.Value); if (deepLinkCreateResult.Success == false) { ReportHub.LogError(ReportCategory.RUNTIME_DEEPLINKS, $"Cannot deserialize deeplink content: {deepLinkCreateResult.ErrorMessage}"); - TryDeleteBridgeFile(); + TryArchiveBridgeFile(); continue; } // A false result means no login flow is awaiting the signin yet: keep the file so it can be picked up later. if (handle.HandleDeepLink(deepLinkCreateResult.Value)) - TryDeleteBridgeFile(); + { + Debug.Log("[DLDBG] Sentinel: deeplink consumed, archiving bridge file"); // TODO: temporary deep-link debug log, remove. + TryArchiveBridgeFile(); + } + else + Debug.Log("[DLDBG] Sentinel: deeplink deferred, keeping bridge file"); // TODO: temporary deep-link debug log, remove. } } - private static void TryDeleteBridgeFile() + // TODO: temporary deep-link debug. Instead of deleting, rename the consumed bridge file with the consuming + // process PID so the traces stay on disk for inspection. Restore File.Delete once the flow is understood. + private static void TryArchiveBridgeFile() { try { - File.Delete(DEEP_LINK_BRIDGE_PATH); + int pid = System.Diagnostics.Process.GetCurrentProcess().Id; + string directory = Path.GetDirectoryName(DEEP_LINK_BRIDGE_PATH)!; + string archivedPath = Path.Combine(directory, $"deeplink-bridge.consumed.pid{pid}.{DateTime.UtcNow:yyyyMMdd-HHmmss-fff}.json"); + + File.Move(DEEP_LINK_BRIDGE_PATH, archivedPath); + Debug.Log($"[DLDBG] Sentinel: archived bridge file -> {archivedPath}"); // TODO: temporary deep-link debug log, remove. } catch (Exception e) { - // Deletion can fail transiently (file locked/rewritten by the launcher). Log and keep the loop alive. - ReportHub.LogError(ReportCategory.RUNTIME_DEEPLINKS, $"Failed to delete deeplink bridge file: {e.Message}"); + // Rename can fail transiently (file locked/rewritten by the launcher). Log and keep the loop alive. + ReportHub.LogError(ReportCategory.RUNTIME_DEEPLINKS, $"Failed to archive deeplink bridge file: {e.Message}"); } } } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs index 90252365444..716ebad5b2d 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs @@ -1,4 +1,5 @@ using System; +using UnityEngine; namespace DCL.RuntimeDeepLink { @@ -11,6 +12,8 @@ public class DeeplinkSigninDispatcher : IDeeplinkSigninDispatcher public IDisposable Subscribe(Action onSigninReceived, string? expectedRequestId = null) { + Debug.Log("[DLDBG] Dispatcher.Subscribe"); // TODO: temporary deep-link debug log, remove. + var subscription = new Subscription(this, onSigninReceived); current = subscription; return subscription; @@ -18,6 +21,8 @@ public IDisposable Subscribe(Action onSigninReceived, string? expectedRe public void Dispatch(string identityId, string? sourceRequestId = null) { + Debug.Log($"[DLDBG] Dispatcher.Dispatch id='{identityId}' hasSubscriber={current != null}"); // TODO: temporary deep-link debug log, remove. + // Stage 1: the sourceRequestId / expectedRequestId correlation is intentionally not yet applied. current?.Handler(identityId); } From bcd19a684534b711ebdd8d2fca33a5f37efde69d Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Wed, 1 Jul 2026 19:54:31 +0200 Subject: [PATCH 07/54] enabled logging categories --- .../Global/AppArgs/AppArgsFlags.cs | 6 +----- .../AppArgs/ApplicationParametersParser.cs | 5 +---- .../ReportsHandlingSettingsDevelopment.asset | 4 ++++ .../ReportsHandlingSettingsProduction.asset | 18 ++++++++++++++++++ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs index 5e0964b0088..0726a3b2bf6 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs @@ -31,11 +31,7 @@ public static class AppArgsFlags /// public const string COMMUNITY = "community"; - /// - /// The opaque identity id delivered by the auth website's signin deep link (decentraland://?signin={identityId}). - /// Parsed in DeepLinkHandle and dispatched to the deep-link login awaiting it. - /// - public const string SIGNIN = "signin"; + public const string SIGNIN = "signin"; /// The opaque identity id delivered by the auth website's signin deep link (decentraland://?signin={identityId}). public const string FORCED_EMOTES = "self-force-emotes"; public const string SELF_PREVIEW_EMOTES = "self-preview-emotes"; diff --git a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs index ebda1917ebc..a2715257e4f 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs @@ -90,10 +90,7 @@ public static Dictionary ProcessDeepLinkParameters(string deepLi { var output = new Dictionary(); - // Drop the optional host segment (e.g. "open" in decentraland://open?signin=... or decentraland://open/?signin=...) - // so only the query remains; otherwise the host fuses into the first key (e.g. "open/?signin"). The optional - // trailing slash covers the real auth-website format (decentraland://open/?signin=...). Legacy host-less links - // such as decentraland://realm=...&position=... are untouched ('=' follows the word, not '?'). + // Drop the optional host segment (e.g. "open" in decentraland://open?signin=... or decentraland://open/?signin=...) so only the query remains; deepLinkString = Regex.Replace(deepLinkString, @"^(decentraland:/+)[A-Za-z][A-Za-z0-9_-]*/*\?", "$1?"); // Update deep link so that Uri class allows the host name diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset index c69c1771a1c..7f9c1cb4c7f 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset @@ -708,6 +708,10 @@ MonoBehaviour: Severity: 2 - Category: MULTIPLAYER Severity: 2 + - Category: RUNTIME_DEEPLINKS + Severity: 3 + - Category: RUNTIME_DEEPLINKS + Severity: 2 sentryMatrix: entries: [] debounceEnabled: 1 diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset index d9d63562f5d..baef4f8fee8 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset @@ -448,6 +448,24 @@ MonoBehaviour: Severity: 0 - Category: MULTIPLAYER Severity: 2 + - Category: UNSPECIFIED + Severity: 3 + - Category: UNSPECIFIED + Severity: 2 + - Category: UNSPECIFIED + Severity: 0 + - Category: AUTHENTICATION + Severity: 3 + - Category: AUTHENTICATION + Severity: 2 + - Category: AUTHENTICATION + Severity: 0 + - Category: RUNTIME_DEEPLINKS + Severity: 3 + - Category: RUNTIME_DEEPLINKS + Severity: 2 + - Category: RUNTIME_DEEPLINKS + Severity: 0 sentryMatrix: entries: - Category: AUDIO_SOURCES From 9750311e12acf42c87ea187e4b560ef64911244f Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Wed, 1 Jul 2026 22:40:53 +0200 Subject: [PATCH 08/54] replaced socket with POST request --- .../Dapp/DappWeb3Authenticator.Deeplink.cs | 101 +++++++++--------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs index 23bc97d11c1..32223afb531 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs @@ -7,6 +7,7 @@ using DCL.Web3.Identities; using DCL.WebRequests; using Nethereum.Signer; +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Globalization; @@ -20,46 +21,35 @@ public partial class DappWeb3Authenticator // deep link back to this process; this can take much longer than a socket round-trip. private const int DEEPLINK_TIMEOUT_SECONDS = 300; - // Deep-link sign-in collaborators. Not yet wired through the constructor: the production wiring - // (web request controller + signin dispatcher) is the next step. + // Deep-link sign-in collaborators, wired through the constructor on the production path (see BootstrapContainer). private readonly IWebRequestController? webRequestController; private readonly IDeeplinkSigninDispatcher? deeplinkSigninDispatcher; /// - /// Identity-based deep-link sign-in. Same setup as (mutex, socket connect, emit - /// "request" to obtain a requestId, open the browser) but the URL carries flow=deeplink and this method - /// does NOT subscribe to the socket "outcome" event. Instead it awaits the - /// for the identityId that the browser delivers via an OS-routed deep link, then resolves the identity - /// via (GET /identities/{id}). + /// Identity-based deep-link sign-in. Mints a requestId with a single POST {authApiUrl}/requests + /// (body: { "method": "dcl_personal_sign", "params": [ephemeralMessage] }), builds the browser URL with + /// flow=deeplink and opens it, then awaits the for the + /// identityId that the browser delivers via an OS-routed deep link, and resolves the identity via + /// (GET /identities/{id}). /// /// The browser owns the ephemeral keypair in this flow: it generates the keypair, builds the full AuthIdentity /// and POSTs it to /identities, ignoring any ephemeral params Unity sends. The final identity is therefore - /// resolved entirely from the fetcher; the local ephemeral generated below is NOT used to build it. The - /// dcl_personal_sign "request" emit is still required: auth-app's RequestPage needs a requestId in - /// its URL path, and the server only mints that requestId in response to the emit. We keep the emit's - /// message byte-identical to (the proven, server-accepted form). + /// resolved entirely from the fetcher; the local ephemeral generated below is NOT used to build it. It only + /// serves as the body of the /requests POST that mints the requestId auth-app's RequestPage needs + /// in its URL path. /// public async UniTask LoginViaDeeplinkAsync(LoginPayload payload, CancellationToken ct) { if (webRequestController == null || deeplinkSigninDispatcher == null) throw new Web3Exception($"{nameof(LoginViaDeeplinkAsync)} requires the web request controller and the signin dispatcher to be wired (production path only)."); + // Serialize with other web3 operations so the shared urlBuilder is not mutated concurrently. await mutex.WaitAsync(ct); -#if !UNITY_WEBGL - SynchronizationContext originalSyncContext = SynchronizationContext.Current; // IGNORE_LINE_WEBGL_THREAD_SAFETY_FLAG -#endif - try { - await UniTask.SwitchToMainThread(ct); - - await ConnectToAuthApiAsync(); - - // Emit "request" exactly like LoginAsync to mint a requestId for the browser URL. The local ephemeral is - // not used to build the final identity (the browser owns the real keypair; we resolve via the fetcher), - // but we still send the standard well-formed message so the server accepts the emit and recover(requestId) - // returns a valid request. + // The local ephemeral is not used to build the final identity (the browser owns the real keypair; we + // resolve via the fetcher), but it is the well-formed message the server signs into the minted request. var ephemeralAccount = web3AccountFactory.CreateRandomAccount(); DateTime sessionExpiration = identityExpirationDuration != null @@ -68,48 +58,53 @@ public async UniTask LoginViaDeeplinkAsync(LoginPayload payload, string ephemeralMessage = CreateEphemeralMessage(ephemeralAccount, sessionExpiration); - SignatureIdResponse authenticationResponse = await RequestEthMethodWithSignatureAsync(new LoginAuthApiRequest - { - method = "dcl_personal_sign", - @params = new object[] { ephemeralMessage }, - }, ct); + CreateRequestResponseDto createRequestResponse = await CreateSigninRequestAsync(webRequestController, ephemeralMessage, ct); + if (string.IsNullOrEmpty(createRequestResponse.requestId)) + throw new Web3Exception("Cannot solve auth request id"); + + // OpenUrl routes through Application.OpenURL, which must run on the main thread. await UniTask.SwitchToMainThread(ct); - string url = $"{signatureWebAppUrl}/{authenticationResponse.requestId}?loginMethod={payload.Method}&flow=deeplink"; + string url = $"{signatureWebAppUrl}/{createRequestResponse.requestId}?loginMethod={payload.Method}&flow=deeplink"; webBrowser.OpenUrl(url); // The browser builds and stores the AuthIdentity, then opens decentraland://?signin={identityId}; - // the OS routes it to DeepLinkHandle, which dispatches it here. We do not listen to socket "outcome". + // the OS routes it to DeepLinkHandle, which dispatches it here. string identityId = await WaitForSigninAsync(deeplinkSigninDispatcher, ct); - IWeb3Identity identity = await FetchIdentityByIdAsync(webRequestController, identityId, IWeb3Identity.Web3IdentitySource.Deeplink, ct); - - await DisconnectFromAuthApiAsync(); - - return identity; - } - catch (Exception) - { - await DisconnectFromAuthApiAsync(); - throw; + return await FetchIdentityByIdAsync(webRequestController, identityId, IWeb3Identity.Web3IdentitySource.Deeplink, ct); } finally { -#if !UNITY_WEBGL - if (originalSyncContext != null) - await UniTask.SwitchToSynchronizationContext(originalSyncContext, CancellationToken.None); - else - await UniTask.SwitchToMainThread(CancellationToken.None); -#else - await UniTask.SwitchToMainThread(CancellationToken.None); -#endif - mutex.Release(); } } + /// + /// Mints a sign-in requestId via POST {authApiUrl}/requests. The browser later recovers the + /// request by that id to drive the wallet signature. + /// + private async UniTask CreateSigninRequestAsync(IWebRequestController requestController, string ephemeralMessage, CancellationToken ct) + { + urlBuilder.Clear(); + + urlBuilder.AppendDomain(URLDomain.FromString(authApiUrl)) + .AppendPath(new URLPath("requests")); + + var commonArguments = new CommonArguments(urlBuilder.Build()); + + string body = JsonConvert.SerializeObject(new LoginAuthApiRequest + { + method = "dcl_personal_sign", + @params = new object[] { ephemeralMessage }, + }); + + return await requestController.PostAsync(commonArguments, GenericPostArguments.CreateJson(body), ct, ReportCategory.AUTHENTICATION) + .CreateFromNewtonsoftJsonAsync(); + } + /// /// Awaits the first identityId the dispatcher delivers, applying a timeout and honoring external /// cancellation. Disposing the subscription on completion clears the dispatcher buffer so a deep link consumed @@ -154,6 +149,14 @@ public async UniTask FetchIdentityByIdAsync(IWebRequestCon return new DecentralandIdentity(new Web3Address(address), ephemeralAccount, expiration, authChain, source); } + [Serializable] + private struct CreateRequestResponseDto + { + public string requestId; + public string expiration; + public int code; + } + [Serializable] private struct IdentityAuthResponseDto { From 233291d55fad1865b6cbf6f1716540a0fa934ffd Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 00:06:10 +0200 Subject: [PATCH 09/54] moved to its own class --- .../Global/Dynamic/BootstrapContainer.cs | 12 +- .../Dynamic/_Scratch/DeeplinkBackendTester.cs | 73 ++---- .../Implementations/CompositeWeb3Provider.cs | 6 +- .../Dapp/DappDeepLinkAuthenticator.cs | 211 ++++++++++++++++++ .../Dapp/DappDeepLinkAuthenticator.cs.meta | 2 + .../Dapp/DappWeb3Authenticator.Deeplink.cs | 182 --------------- .../DappWeb3Authenticator.Deeplink.cs.meta | 2 - .../Dapp/DappWeb3Authenticator.cs | 8 +- 8 files changed, 243 insertions(+), 253 deletions(-) create mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs create mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs.meta delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs.meta diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index c6c338fbae6..93197692a96 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -223,12 +223,22 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies( new HashSet(sceneLoaderSettings.Web3ReadOnlyMethods), dclEnvironment, new AuthCodeVerificationFeatureFlag(), + identityExpirationDuration + ); + + // Deep-link sign-in is a self-contained authenticator; the socket-based DappWeb3Authenticator above stays + // responsible for the IEthereumApi signature/confirmation flow only. + var dappDeepLinkAuth = new DappDeepLinkAuthenticator( + webBrowser, + URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.ApiAuth)), + URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.AuthSignatureWebApp)), + web3AccountFactory, webRequestController, deeplinkSigninDispatcher, identityExpirationDuration ); - ICompositeWeb3Provider result = new CompositeWeb3Provider(thirdWebAuth, dappAuth, identityCache, container.Controller); + ICompositeWeb3Provider result = new CompositeWeb3Provider(thirdWebAuth, dappAuth, dappDeepLinkAuth, identityCache, container.Controller); return result; } diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs index cdd57abb6cf..65aa7b5c580 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs @@ -1,24 +1,22 @@ // THROWAWAY scratch tool — manual verification of deep-link sign-in backend communication. // NOT production code. No style/architecture rules apply here. Do not commit / do not move into a prod path. -// Sits in a _Scratch subfolder next to BootstrapContainer.cs (which constructs DappWeb3Authenticator with the +// Sits in a _Scratch subfolder next to BootstrapContainer.cs (which constructs DappDeepLinkAuthenticator with the // exact same deps), so it lands in Assembly-CSharp and every type resolves. Wrapped in #if UNITY_EDITOR so // Unity never compiles it into a player build. Do not commit; delete when done. // -// HOW TO USE (Play Mode required — socket.io, web requests, OpenUrl): +// HOW TO USE (Play Mode required — web requests, OpenUrl): // 1. Empty scene -> empty GameObject -> attach this component. // 2. Enter Play Mode (Awake builds the authenticator). // 3. Right-click the component header (or the gear icon) to get the context menu, then run the steps in order. // Read the Unity console between steps. // // STEPS (context-menu items): -// "1. Connect + Request" -> full LoginViaDeeplinkAsync: socket connect to auth-api + emit "request" -// (mints requestId) + opens the browser with flow=deeplink. Reaching the browser -// proves the socket handshake worked. Then it BLOCKS waiting for an identityId. +// "1. Login (deeplink)" -> full LoginAsync: POST /requests (mints requestId over HTTP) + opens the browser with +// flow=deeplink. Reaching the browser proves the mint worked. Then it BLOCKS waiting +// for an identityId. // "2. Dispatch identityId" -> paste an identityId into the Inspector field first, then run this to // dispatcher.Dispatch(it). Replaces OS deep-link routing; unblocks step 1, which // then does GET /identities/{id} internally (full E2E). -// "3. Fetch identity by id"-> independent REST check: GET /identities/{id} via FetchIdentityByIdAsync directly -// (uses the Inspector identityId field). // "Cancel current" -> cancels the in-flight operation and resets the token. #if UNITY_EDITOR @@ -38,7 +36,6 @@ using DCL.WebRequests.RequestsHub; using DCL.DebugUtilities.UIBindings; using System; -using System.Collections.Generic; using System.Threading; using UnityEngine; @@ -46,21 +43,19 @@ public class DeeplinkBackendTester : MonoBehaviour { private const DecentralandEnvironment ENVIRONMENT = DecentralandEnvironment.Zone; - [Tooltip("Paste the identityId here before running steps 2 and 3.")] + [Tooltip("Paste the identityId here before running step 2.")] [SerializeField] private string identityId = ""; - private DappWeb3Authenticator authenticator; + private DappDeepLinkAuthenticator authenticator; private DeeplinkSigninDispatcher dispatcher; private IWebRequestController webRequestController; private CancellationTokenSource cts; - private string authApiBaseUrl; private void Awake() { cts = new CancellationTokenSource(); var urls = DecentralandUrlsSource.CreateForTest(ENVIRONMENT, ILaunchMode.PLAY); - authApiBaseUrl = urls.Url(DecentralandUrl.ApiAuth); dispatcher = new DeeplinkSigninDispatcher(); @@ -73,25 +68,18 @@ private void Awake() var webBrowser = new UnityAppWebBrowser(urls); - authenticator = new DappWeb3Authenticator( + authenticator = new DappDeepLinkAuthenticator( webBrowser, URLAddress.FromString(urls.Url(DecentralandUrl.ApiAuth)), URLAddress.FromString(urls.Url(DecentralandUrl.AuthSignatureWebApp)), - URLDomain.FromString(urls.Url(DecentralandUrl.ApiRpc)), - new MemoryWeb3IdentityCache(), // not touched in deeplink path new Web3AccountFactory(), - new HashSet(), // whitelistMethods — not read in deeplink path - new HashSet(), // readOnlyMethods — not read in deeplink path - ENVIRONMENT, - new NoopCodeVerificationFeatureFlag(), webRequestController, dispatcher, null); // identityExpirationDuration Debug.Log($"[DeeplinkBackendTester] Ready. env={ENVIRONMENT}\n" + - $" auth-api (socket): {urls.Url(DecentralandUrl.ApiAuth)}\n" + - $" signature (browser): {urls.Url(DecentralandUrl.AuthSignatureWebApp)}\n" + - $" rpc : {urls.Url(DecentralandUrl.ApiRpc)}"); + $" auth-api (POST) : {urls.Url(DecentralandUrl.ApiAuth)}\n" + + $" signature (browser): {urls.Url(DecentralandUrl.AuthSignatureWebApp)}"); } private void OnDestroy() @@ -101,7 +89,7 @@ private void OnDestroy() authenticator?.Dispose(); } - [ContextMenu("1. Connect + Request (LoginViaDeeplink)")] + [ContextMenu("1. Login (deeplink)")] private void Step1_Login() { if (!EnsurePlayMode()) return; @@ -125,13 +113,6 @@ private void Step2_Dispatch() dispatcher.Dispatch(id); } - [ContextMenu("3. Fetch identity by id (GET /identities/{id})")] - private void Step3_Fetch() - { - if (!EnsurePlayMode()) return; - RunFetchAsync().Forget(); - } - [ContextMenu("Cancel current")] private void CancelCurrent() { @@ -145,40 +126,17 @@ private void CancelCurrent() private async UniTaskVoid RunLoginAsync() { - Debug.Log("[DeeplinkBackendTester] >> [1] LoginViaDeeplinkAsync START (socket connect + emit request + open browser, then waits for dispatch)"); + Debug.Log("[DeeplinkBackendTester] >> [1] LoginAsync START (POST /requests + open browser, then waits for dispatch)"); try { - IWeb3Identity identity = await authenticator.LoginViaDeeplinkAsync(LoginPayload.ForDappFlow(LoginMethod.GOOGLE), cts.Token); + IWeb3Identity identity = await authenticator.LoginAsync(LoginPayload.ForDappFlow(LoginMethod.GOOGLE), cts.Token); Debug.Log($"[DeeplinkBackendTester] << [1] LOGIN OK. address={identity.Address} expiration={identity.Expiration:O} source={identity.Source} isExpired={identity.IsExpired}"); } catch (OperationCanceledException) { Debug.Log("[DeeplinkBackendTester] << [1] cancelled"); } catch (Exception e) { Debug.LogError($"[DeeplinkBackendTester] << [1] EXCEPTION: {e.GetType().Name}: {e.Message}\n{e}"); } } - private async UniTaskVoid RunFetchAsync() - { - string id = (identityId ?? "").Trim(); - - if (string.IsNullOrEmpty(id)) - { - Debug.LogWarning("[DeeplinkBackendTester] [3] identityId field is empty."); - return; - } - - Debug.Log($"[DeeplinkBackendTester] >> [3] FetchIdentityByIdAsync(\"{id}\") GET {authApiBaseUrl}/identities/{id}"); - - try - { - IWeb3Identity identity = await authenticator.FetchIdentityByIdAsync( - webRequestController, id, IWeb3Identity.Web3IdentitySource.Deeplink, cts.Token); - - Debug.Log($"[DeeplinkBackendTester] << [3] FETCH OK. address={identity.Address} expiration={identity.Expiration:O} source={identity.Source} isExpired={identity.IsExpired}"); - } - catch (OperationCanceledException) { Debug.Log("[DeeplinkBackendTester] << [3] cancelled"); } - catch (Exception e) { Debug.LogError($"[DeeplinkBackendTester] << [3] EXCEPTION: {e.GetType().Name}: {e.Message}\n{e}"); } - } - private bool EnsurePlayMode() { if (authenticator != null) return true; @@ -186,10 +144,5 @@ private bool EnsurePlayMode() Debug.LogWarning("[DeeplinkBackendTester] Not initialized — enter Play Mode first (Awake builds the authenticator)."); return false; } - - private sealed class NoopCodeVerificationFeatureFlag : DappWeb3Authenticator.ICodeVerificationFeatureFlag - { - public bool ShouldWaitForCodeVerificationFromServer => false; - } } #endif diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs index b6d56126216..e18c5e7db50 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs @@ -18,6 +18,7 @@ public class CompositeWeb3Provider : ICompositeWeb3Provider { private readonly ThirdWebAuthenticator thirdWebAuth; private readonly DappWeb3Authenticator dappAuth; + private readonly IWeb3Authenticator dappLogin; private readonly IWeb3IdentityCache identityCache; private readonly IAnalyticsController analytics; @@ -38,17 +39,19 @@ public event Action? OTPSendSucceeded public bool IsThirdWebOTP => CurrentProvider == AuthProvider.ThirdWeb; - private IWeb3Authenticator currentAuthenticator => CurrentProvider == AuthProvider.ThirdWeb ? thirdWebAuth : dappAuth; + private IWeb3Authenticator currentAuthenticator => CurrentProvider == AuthProvider.ThirdWeb ? thirdWebAuth : dappLogin; private IEthereumApi currentEthereumApi => CurrentProvider == AuthProvider.ThirdWeb ? thirdWebAuth : dappAuth; public CompositeWeb3Provider( ThirdWebAuthenticator thirdWebAuth, DappWeb3Authenticator dappAuth, + DappDeepLinkAuthenticator dappLogin, IWeb3IdentityCache identityCache, IAnalyticsController analytics) { this.thirdWebAuth = thirdWebAuth ?? throw new ArgumentNullException(nameof(thirdWebAuth)); this.dappAuth = dappAuth ?? throw new ArgumentNullException(nameof(dappAuth)); + this.dappLogin = dappLogin ?? throw new ArgumentNullException(nameof(dappLogin)); this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache)); this.analytics = analytics ?? throw new ArgumentNullException(nameof(analytics)); } @@ -57,6 +60,7 @@ public void Dispose() { thirdWebAuth.Dispose(); dappAuth.Dispose(); + dappLogin.Dispose(); identityCache.Dispose(); } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs new file mode 100644 index 00000000000..fb7a5d38b09 --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -0,0 +1,211 @@ +using CommunicationData.URLHelpers; +using Cysharp.Threading.Tasks; +using DCL.Browser; +using DCL.Diagnostics; +using DCL.RuntimeDeepLink; +using DCL.Web3.Abstract; +using DCL.Web3.Chains; +using DCL.Web3.Identities; +using DCL.WebRequests; +using Nethereum.Signer; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; + +namespace DCL.Web3.Authenticators +{ + /// + /// Identity-based deep-link sign-in. Mints a requestId with a single POST {authApiUrl}/requests + /// (body: { "method": "dcl_personal_sign", "params": [ephemeralMessage] }), builds the browser URL with + /// flow=deeplink and opens it, then awaits the for the + /// identityId that the browser delivers via an OS-routed deep link, and resolves the identity via + /// GET /identities/{id}. + /// + /// The browser owns the ephemeral keypair in this flow: it generates the keypair, builds the full AuthIdentity + /// and POSTs it to /identities, ignoring any ephemeral params Unity sends. The final identity is therefore + /// resolved entirely from the fetcher; the local ephemeral generated below is NOT used to build it. It only + /// serves as the body of the /requests POST that mints the requestId auth-app's RequestPage needs + /// in its URL path. + /// + public class DappDeepLinkAuthenticator : IWeb3Authenticator + { + // Fallback session length when no explicit override is provided (days). + private const double IDENTITY_EXPIRATION_PERIOD = 30; + + // The deep-link flow waits for the user to sign in their browser and for the OS to route the resulting + // deep link back to this process; this can take much longer than a socket round-trip. + private const int DEEPLINK_TIMEOUT_SECONDS = 300; + + private readonly IWebBrowser webBrowser; + private readonly URLAddress authApiUrl; + private readonly URLAddress signatureWebAppUrl; + private readonly IWeb3AccountFactory web3AccountFactory; + private readonly IWebRequestController webRequestController; + private readonly IDeeplinkSigninDispatcher deeplinkSigninDispatcher; + private readonly int? identityExpirationDuration; + private readonly URLBuilder urlBuilder = new (); + + public DappDeepLinkAuthenticator( + IWebBrowser webBrowser, + URLAddress authApiUrl, + URLAddress signatureWebAppUrl, + IWeb3AccountFactory web3AccountFactory, + IWebRequestController webRequestController, + IDeeplinkSigninDispatcher deeplinkSigninDispatcher, + int? identityExpirationDuration = null) + { + this.webBrowser = webBrowser; + this.authApiUrl = authApiUrl; + this.signatureWebAppUrl = signatureWebAppUrl; + this.web3AccountFactory = web3AccountFactory; + this.webRequestController = webRequestController; + this.deeplinkSigninDispatcher = deeplinkSigninDispatcher; + this.identityExpirationDuration = identityExpirationDuration; + } + + public void Dispose() { } + + public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) + { + // The local ephemeral is not used to build the final identity (the browser owns the real keypair; we + // resolve via the fetcher), but it is the well-formed message the server signs into the minted request. + var ephemeralAccount = web3AccountFactory.CreateRandomAccount(); + + DateTime sessionExpiration = identityExpirationDuration != null + ? DateTime.UtcNow.AddSeconds(identityExpirationDuration.Value) + : DateTime.UtcNow.AddDays(IDENTITY_EXPIRATION_PERIOD); + + string ephemeralMessage = CreateEphemeralMessage(ephemeralAccount, sessionExpiration); + + CreateRequestResponseDto createRequestResponse = await CreateSigninRequestAsync(ephemeralMessage, ct); + + if (string.IsNullOrEmpty(createRequestResponse.requestId)) + throw new Web3Exception("Cannot solve auth request id"); + + // OpenUrl routes through Application.OpenURL, which must run on the main thread. + await UniTask.SwitchToMainThread(ct); + + string url = $"{signatureWebAppUrl}/{createRequestResponse.requestId}?loginMethod={payload.Method}&flow=deeplink"; + + webBrowser.OpenUrl(url); + + // The browser builds and stores the AuthIdentity, then opens decentraland://?signin={identityId}; + // the OS routes it to DeepLinkHandle, which dispatches it here. + string identityId = await WaitForSigninAsync(ct); + + return await FetchIdentityByIdAsync(identityId, ct); + } + + public UniTask LogoutAsync(CancellationToken ct) => + UniTask.CompletedTask; + + /// + /// Mints a sign-in requestId via POST {authApiUrl}/requests. The browser later recovers the + /// request by that id to drive the wallet signature. + /// + private async UniTask CreateSigninRequestAsync(string ephemeralMessage, CancellationToken ct) + { + urlBuilder.Clear(); + + urlBuilder.AppendDomain(URLDomain.FromString(authApiUrl)) + .AppendPath(new URLPath("requests")); + + var commonArguments = new CommonArguments(urlBuilder.Build()); + + string body = JsonConvert.SerializeObject(new SigninRequestDto + { + method = "dcl_personal_sign", + @params = new object[] { ephemeralMessage }, + }); + + return await webRequestController.PostAsync(commonArguments, GenericPostArguments.CreateJson(body), ct, ReportCategory.AUTHENTICATION) + .CreateFromNewtonsoftJsonAsync(); + } + + /// + /// Awaits the first identityId the dispatcher delivers, applying a timeout and honoring external + /// cancellation. Disposing the subscription on completion clears the dispatcher buffer so a deep link consumed + /// (or cancelled) by one attempt does not bleed into the next. + /// + private async UniTask WaitForSigninAsync(CancellationToken ct) + { + var completionSource = new UniTaskCompletionSource(); + + using IDisposable subscription = deeplinkSigninDispatcher.Subscribe(identityId => completionSource.TrySetResult(identityId)); + + // Realtime, not the default DeltaTime: the wait spans the user switching to the browser and back, during + // which the app is backgrounded and game time stalls. Only wall-clock measures the deadline correctly. + return await completionSource.Task.Timeout(TimeSpan.FromSeconds(DEEPLINK_TIMEOUT_SECONDS), DelayType.Realtime).AttachExternalCancellation(ct); + } + + /// + /// Fetches a stored identity by its opaque id via GET {authApiUrl}/identities/{id} and reconstructs a + /// fully-formed . + /// + private async UniTask FetchIdentityByIdAsync(string identityId, CancellationToken ct) + { + urlBuilder.Clear(); + + urlBuilder.AppendDomain(URLDomain.FromString(authApiUrl)) + .AppendPath(new URLPath($"identities/{identityId}")); + + var commonArguments = new CommonArguments(urlBuilder.Build()); + + IdentityAuthResponseDto json = await webRequestController.GetAsync(commonArguments, ct, ReportCategory.AUTHENTICATION) + .CreateFromNewtonsoftJsonAsync(); + + var authChain = AuthChain.Create(); + + foreach (AuthLink authLink in json.identity.authChain) + authChain.Set(authLink); + + string address = authChain.Get(AuthLinkType.SIGNER).payload; + IWeb3Account ephemeralAccount = web3AccountFactory.CreateAccount(new EthECKey(json.identity.ephemeralIdentity.privateKey)); + DateTime expiration = DateTime.Parse(json.identity.expiration, null, DateTimeStyles.RoundtripKind); + + return new DecentralandIdentity(new Web3Address(address), ephemeralAccount, expiration, authChain, IWeb3Identity.Web3IdentitySource.Deeplink); + } + + private string CreateEphemeralMessage(IWeb3Account ephemeralAccount, DateTime expiration) => + $"Decentraland Login\nEphemeral address: {ephemeralAccount.Address.OriginalFormat}\nExpiration: {expiration:yyyy-MM-ddTHH:mm:ss.fffZ}"; + + [Serializable] + private struct SigninRequestDto + { + public string method; + public object[] @params; + } + + [Serializable] + private struct CreateRequestResponseDto + { + public string requestId; + public string expiration; + public int code; + } + + [Serializable] + private struct IdentityAuthResponseDto + { + public IdentityDto identity; + + [Serializable] + public struct IdentityDto + { + public string expiration; + public EphemeralIdentityDto ephemeralIdentity; + public List authChain; + } + + [Serializable] + public struct EphemeralIdentityDto + { + public string address; + public string privateKey; + public string publicKey; + } + } + } +} diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs.meta new file mode 100644 index 00000000000..4b72427151c --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 91ddac7c757b5e74c9d9a9ead64da6dd \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs deleted file mode 100644 index 32223afb531..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs +++ /dev/null @@ -1,182 +0,0 @@ -using CommunicationData.URLHelpers; -using Cysharp.Threading.Tasks; -using DCL.Diagnostics; -using DCL.RuntimeDeepLink; -using DCL.Web3.Abstract; -using DCL.Web3.Chains; -using DCL.Web3.Identities; -using DCL.WebRequests; -using Nethereum.Signer; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading; - -namespace DCL.Web3.Authenticators -{ - public partial class DappWeb3Authenticator - { - // The deep-link flow waits for the user to sign in their browser and for the OS to route the resulting - // deep link back to this process; this can take much longer than a socket round-trip. - private const int DEEPLINK_TIMEOUT_SECONDS = 300; - - // Deep-link sign-in collaborators, wired through the constructor on the production path (see BootstrapContainer). - private readonly IWebRequestController? webRequestController; - private readonly IDeeplinkSigninDispatcher? deeplinkSigninDispatcher; - - /// - /// Identity-based deep-link sign-in. Mints a requestId with a single POST {authApiUrl}/requests - /// (body: { "method": "dcl_personal_sign", "params": [ephemeralMessage] }), builds the browser URL with - /// flow=deeplink and opens it, then awaits the for the - /// identityId that the browser delivers via an OS-routed deep link, and resolves the identity via - /// (GET /identities/{id}). - /// - /// The browser owns the ephemeral keypair in this flow: it generates the keypair, builds the full AuthIdentity - /// and POSTs it to /identities, ignoring any ephemeral params Unity sends. The final identity is therefore - /// resolved entirely from the fetcher; the local ephemeral generated below is NOT used to build it. It only - /// serves as the body of the /requests POST that mints the requestId auth-app's RequestPage needs - /// in its URL path. - /// - public async UniTask LoginViaDeeplinkAsync(LoginPayload payload, CancellationToken ct) - { - if (webRequestController == null || deeplinkSigninDispatcher == null) - throw new Web3Exception($"{nameof(LoginViaDeeplinkAsync)} requires the web request controller and the signin dispatcher to be wired (production path only)."); - - // Serialize with other web3 operations so the shared urlBuilder is not mutated concurrently. - await mutex.WaitAsync(ct); - - try - { - // The local ephemeral is not used to build the final identity (the browser owns the real keypair; we - // resolve via the fetcher), but it is the well-formed message the server signs into the minted request. - var ephemeralAccount = web3AccountFactory.CreateRandomAccount(); - - DateTime sessionExpiration = identityExpirationDuration != null - ? DateTime.UtcNow.AddSeconds(identityExpirationDuration.Value) - : DateTime.UtcNow.AddDays(IDENTITY_EXPIRATION_PERIOD); - - string ephemeralMessage = CreateEphemeralMessage(ephemeralAccount, sessionExpiration); - - CreateRequestResponseDto createRequestResponse = await CreateSigninRequestAsync(webRequestController, ephemeralMessage, ct); - - if (string.IsNullOrEmpty(createRequestResponse.requestId)) - throw new Web3Exception("Cannot solve auth request id"); - - // OpenUrl routes through Application.OpenURL, which must run on the main thread. - await UniTask.SwitchToMainThread(ct); - - string url = $"{signatureWebAppUrl}/{createRequestResponse.requestId}?loginMethod={payload.Method}&flow=deeplink"; - - webBrowser.OpenUrl(url); - - // The browser builds and stores the AuthIdentity, then opens decentraland://?signin={identityId}; - // the OS routes it to DeepLinkHandle, which dispatches it here. - string identityId = await WaitForSigninAsync(deeplinkSigninDispatcher, ct); - - return await FetchIdentityByIdAsync(webRequestController, identityId, IWeb3Identity.Web3IdentitySource.Deeplink, ct); - } - finally - { - mutex.Release(); - } - } - - /// - /// Mints a sign-in requestId via POST {authApiUrl}/requests. The browser later recovers the - /// request by that id to drive the wallet signature. - /// - private async UniTask CreateSigninRequestAsync(IWebRequestController requestController, string ephemeralMessage, CancellationToken ct) - { - urlBuilder.Clear(); - - urlBuilder.AppendDomain(URLDomain.FromString(authApiUrl)) - .AppendPath(new URLPath("requests")); - - var commonArguments = new CommonArguments(urlBuilder.Build()); - - string body = JsonConvert.SerializeObject(new LoginAuthApiRequest - { - method = "dcl_personal_sign", - @params = new object[] { ephemeralMessage }, - }); - - return await requestController.PostAsync(commonArguments, GenericPostArguments.CreateJson(body), ct, ReportCategory.AUTHENTICATION) - .CreateFromNewtonsoftJsonAsync(); - } - - /// - /// Awaits the first identityId the dispatcher delivers, applying a timeout and honoring external - /// cancellation. Disposing the subscription on completion clears the dispatcher buffer so a deep link consumed - /// (or cancelled) by one attempt does not bleed into the next. - /// - private async UniTask WaitForSigninAsync(IDeeplinkSigninDispatcher dispatcher, CancellationToken ct) - { - var completionSource = new UniTaskCompletionSource(); - - using IDisposable subscription = dispatcher.Subscribe(identityId => completionSource.TrySetResult(identityId)); - - // Realtime, not the default DeltaTime: the wait spans the user switching to the browser and back, during - // which the app is backgrounded and game time stalls. Only wall-clock measures the deadline correctly. - return await completionSource.Task.Timeout(TimeSpan.FromSeconds(DEEPLINK_TIMEOUT_SECONDS), DelayType.Realtime).AttachExternalCancellation(ct); - } - - /// - /// Fetches a stored identity by its opaque id via GET {authApiUrl}/identities/{id} and reconstructs a - /// fully-formed . - /// - public async UniTask FetchIdentityByIdAsync(IWebRequestController requestController, string identityId, IWeb3Identity.Web3IdentitySource source, CancellationToken ct) - { - urlBuilder.Clear(); - - urlBuilder.AppendDomain(URLDomain.FromString(authApiUrl)) - .AppendPath(new URLPath($"identities/{identityId}")); - - var commonArguments = new CommonArguments(urlBuilder.Build()); - - IdentityAuthResponseDto json = await requestController.GetAsync(commonArguments, ct, ReportCategory.AUTHENTICATION) - .CreateFromNewtonsoftJsonAsync(); - - var authChain = AuthChain.Create(); - - foreach (AuthLink authLink in json.identity.authChain) - authChain.Set(authLink); - - string address = authChain.Get(AuthLinkType.SIGNER).payload; - IWeb3Account ephemeralAccount = web3AccountFactory.CreateAccount(new EthECKey(json.identity.ephemeralIdentity.privateKey)); - DateTime expiration = DateTime.Parse(json.identity.expiration, null, DateTimeStyles.RoundtripKind); - - return new DecentralandIdentity(new Web3Address(address), ephemeralAccount, expiration, authChain, source); - } - - [Serializable] - private struct CreateRequestResponseDto - { - public string requestId; - public string expiration; - public int code; - } - - [Serializable] - private struct IdentityAuthResponseDto - { - public IdentityDto identity; - - [Serializable] - public struct IdentityDto - { - public string expiration; - public EphemeralIdentityDto ephemeralIdentity; - public List authChain; - } - - [Serializable] - public struct EphemeralIdentityDto - { - public string address; - public string privateKey; - public string publicKey; - } - } - } -} diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs.meta deleted file mode 100644 index 8dd48fce069..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Deeplink.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d02f096ad88773b4f9cd09adac95726e \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs index 0b0c16877b5..58c56e6460b 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs @@ -2,11 +2,9 @@ using Cysharp.Threading.Tasks; using DCL.Browser; using DCL.Multiplayer.Connections.DecentralandUrls; -using DCL.RuntimeDeepLink; using DCL.Web3.Abstract; using DCL.Web3.Chains; using DCL.Web3.Identities; -using DCL.WebRequests; using Newtonsoft.Json; using SocketIOClient; using SocketIOClient.Newtonsoft.Json; @@ -67,8 +65,6 @@ public DappWeb3Authenticator(IWebBrowser webBrowser, HashSet whitelistMethods, HashSet readOnlyMethods, DecentralandEnvironment environment, ICodeVerificationFeatureFlag codeVerificationFeatureFlag, - IWebRequestController? webRequestController = null, - IDeeplinkSigninDispatcher? deeplinkSigninDispatcher = null, int? identityExpirationDuration = null) { this.webBrowser = webBrowser; @@ -81,8 +77,6 @@ public DappWeb3Authenticator(IWebBrowser webBrowser, this.readOnlyMethods = readOnlyMethods; this.environment = environment; this.codeVerificationFeatureFlag = codeVerificationFeatureFlag; - this.webRequestController = webRequestController; - this.deeplinkSigninDispatcher = deeplinkSigninDispatcher; this.identityExpirationDuration = identityExpirationDuration; } @@ -147,7 +141,7 @@ public async UniTask SendAsync(EthApiRequest request, Web3Reques } public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) => - await LoginViaDeeplinkAsync(payload, ct); + await LoginAsync2(payload, ct); /// /// 1. An authentication request is sent to the server From 2cf2a27edd539bbd35632a4f4f14c218a2ebb00a Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 13:00:49 +0200 Subject: [PATCH 10/54] clean up from Dapp old flow --- .../AuthenticationScreenController.cs | 1 - .../IdentityVerificationDappAuthState.cs | 156 ------------------ .../IdentityVerificationDappAuthState.cs.meta | 3 - .../States/LoginSelectionAuthState.cs | 1 - .../Global/Dynamic/BootstrapContainer.cs | 6 - .../Authenticators/ICompositeWeb3Provider.cs | 4 +- .../IDappVerificationHandler.cs | 17 -- .../IDappVerificationHandler.cs.meta | 2 - .../Implementations/CompositeWeb3Provider.cs | 11 +- .../Dapp/DappWeb3Authenticator.Default.cs | 21 +-- ...henticator.ICodeVerificationFeatureFlag.cs | 10 -- ...cator.ICodeVerificationFeatureFlag.cs.meta | 2 - ...appWeb3Authenticator.VerificationStatus.cs | 13 -- ...b3Authenticator.VerificationStatus.cs.meta | 2 - .../Dapp/DappWeb3Authenticator.cs | 75 +-------- 15 files changed, 14 insertions(+), 310 deletions(-) delete mode 100644 Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappAuthState.cs delete mode 100644 Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappAuthState.cs.meta delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/IDappVerificationHandler.cs delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/IDappVerificationHandler.cs.meta delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.ICodeVerificationFeatureFlag.cs delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.ICodeVerificationFeatureFlag.cs.meta delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.VerificationStatus.cs delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.VerificationStatus.cs.meta diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs index 334e22b185f..7dff4a6fb2f 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs @@ -165,7 +165,6 @@ protected override void OnViewInstantiated() new InitAuthState(viewInstance, installSource), new LoginSelectionAuthState(fsm, viewInstance, this, CurrentState, splashScreen, web3Authenticator, webBrowser, enableEmailOTP), new ProfileFetchingAuthState(fsm, viewInstance, this, CurrentState, selfProfile, storedIdentityProvider), - new IdentityVerificationDappAuthState(fsm, viewInstance, this, CurrentState, web3Authenticator), new IdentityVerificationDappDeepLinkAuthState(fsm, viewInstance, this, web3Authenticator), new LobbyForExistingAccountAuthState(fsm, viewInstance, this, splashScreen, CurrentState, characterPreviewController), new LobbyForNewAccountAuthState(fsm, viewInstance, this, CurrentState, characterPreviewController, selfProfile, wearablesProvider, webBrowser, webRequestController, decentralandUrlsSource, profileChangesBus) diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappAuthState.cs deleted file mode 100644 index fe8289c4c23..00000000000 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappAuthState.cs +++ /dev/null @@ -1,156 +0,0 @@ -using Cysharp.Threading.Tasks; -using DCL.Diagnostics; -using DCL.PerformanceAndDiagnostics; -using DCL.Utilities; -using DCL.Web3; -using DCL.Web3.Authenticators; -using DCL.Web3.Identities; -using MVC; -using Plugins.NativeWindowManager; -using System; -using System.Threading; -using static DCL.AuthenticationScreenFlow.AuthenticationScreenController; -using static DCL.UI.UIAnimationHashes; - -namespace DCL.AuthenticationScreenFlow -{ - public class IdentityVerificationDappAuthState : AuthStateBase, IPayloadedState<(LoginMethod method, CancellationToken ct)> - { - private readonly MVCStateMachine machine; - private readonly VerificationDappAuthView view; - private readonly AuthenticationScreenController controller; - private readonly ReactiveProperty currentState; - private readonly ICompositeWeb3Provider compositeWeb3Provider; - - private Exception? loginException; - - public IdentityVerificationDappAuthState( - MVCStateMachine machine, - AuthenticationScreenView viewInstance, - AuthenticationScreenController controller, - ReactiveProperty currentState, - ICompositeWeb3Provider compositeWeb3Provider) : base(viewInstance) - { - this.machine = machine; - view = viewInstance.VerificationDappAuthView; - this.controller = controller; - this.currentState = currentState; - this.compositeWeb3Provider = compositeWeb3Provider; - } - - public void Enter((LoginMethod method, CancellationToken ct) payload) - { - base.Enter(); - - loginException = null; - - // Checks the current screen mode because it could have been overridden with Alt+Enter - NativeWindowManager.RequestTemporaryWindowMode(); - - controller.CurrentRequestID = string.Empty; - - AuthenticateAsync(payload.method, payload.ct).Forget(); - } - - public override void Exit() - { - if (loginException == null) - { - view.Hide(OUT); - - // Hide login selection view if still visible (social logins skip the verification step, - // so ShowVerification is never called and the login selection view remains active) - if (viewInstance.LoginSelectionAuthView.gameObject.activeSelf) - viewInstance.LoginSelectionAuthView.Hide(); - } - else - { - if (currentState.Value == AuthStatus.VerificationRequested) - view.Hide(SLIDE); - - spanErrorInfo = loginException switch - { - OperationCanceledException => new SpanErrorInfo("Login process was cancelled by user"), - SignatureExpiredException ex => new SpanErrorInfo("Web3 signature expired during authentication", ex), - Web3SignatureException ex => new SpanErrorInfo("Web3 signature validation failed", ex), - CodeVerificationException ex => new SpanErrorInfo("Code verification failed during authentication", ex), - Web3Exception ex => new SpanErrorInfo("Connection error during authentication flow", ex), - Exception ex => new SpanErrorInfo("Unexpected error during authentication flow", ex), - }; - - if (loginException is not OperationCanceledException) - ReportHub.LogException(loginException, new ReportData(ReportCategory.AUTHENTICATION)); - } - - NativeWindowManager.ReleaseTemporaryWindowMode(); - view.BackButton.onClick.RemoveListener(controller.CancelLoginProcess); - base.Exit(); - } - - private async UniTaskVoid AuthenticateAsync(LoginMethod method, CancellationToken ct) - { - try - { - compositeWeb3Provider.VerificationRequired += ShowVerification; - IWeb3Identity identity = await compositeWeb3Provider.LoginAsync(LoginPayload.ForDappFlow(method), ct); - machine.Enter(new (identity, false, ct)); - } - catch (OperationCanceledException e) - { - loginException = e; - machine.Enter(SLIDE); - } - catch (SignatureExpiredException e) - { - loginException = e; - machine.Enter(SLIDE); - } - catch (Web3SignatureException e) - { - loginException = e; - machine.Enter(SLIDE); - } - catch (CodeVerificationException e) - { - loginException = e; - machine.Enter(SLIDE); - } - catch (Web3Exception e) - { - loginException = e; - machine.Enter(ErrorType.CONNECTION_ERROR); - } - catch (Exception e) - { - loginException = e; - machine.Enter(ErrorType.CONNECTION_ERROR); - } - finally - { - compositeWeb3Provider.VerificationRequired -= ShowVerification; - } - } - - private void ShowVerification((int code, DateTime expiration, string requestId) data) - { - compositeWeb3Provider.VerificationRequired -= ShowVerification; - - controller.CurrentRequestID = data.requestId; - currentState.Value = AuthStatus.VerificationRequested; - - SentryTransactionNameMapping.Instance.StartSpan(LOADING_TRANSACTION_NAME, new SpanData - { - SpanName = "CodeVerification", - SpanOperation = "auth.code_verification", - Depth = STATE_SPAN_DEPTH + 1, - }); - - // Hide non-interactable Login Screen - viewInstance.LoginSelectionAuthView.Hide(); - - // Show Verification Screen - view.Show(data.code, data.expiration); - view.BackButton.onClick.AddListener(controller.CancelLoginProcess); - } - } -} diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappAuthState.cs.meta b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappAuthState.cs.meta deleted file mode 100644 index 8198611de28..00000000000 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappAuthState.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: a598c3ba1e1c43c7bb50b4629218ecee -timeCreated: 1766517747 \ No newline at end of file diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs index 948868c760e..06a651cb3e8 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs @@ -174,7 +174,6 @@ private void Login(LoginMethod method) currentState.Value = AuthStatus.LoginRequested; view.SetLoadingSpinnerVisibility(true); - // machine.Enter((method, controller.GetRestartedLoginToken())); machine.Enter((method, controller.GetRestartedLoginToken())); } diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index 93197692a96..1e91ab69a54 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -222,7 +222,6 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies( new HashSet(sceneLoaderSettings.Web3WhitelistMethods), new HashSet(sceneLoaderSettings.Web3ReadOnlyMethods), dclEnvironment, - new AuthCodeVerificationFeatureFlag(), identityExpirationDuration ); @@ -272,11 +271,6 @@ private static IReportsHandlingSettings ProvideReportHandlingSettingsAsync(Boots } } - internal class AuthCodeVerificationFeatureFlag : DappWeb3Authenticator.ICodeVerificationFeatureFlag - { - public bool ShouldWaitForCodeVerificationFromServer => FeatureFlagsConfiguration.Instance.IsEnabled(FeatureFlagsStrings.AUTH_CODE_VALIDATION); - } - [Serializable] public class BootstrapSettings : IDCLPluginSettings { diff --git a/Explorer/Assets/DCL/Web3/Authenticators/ICompositeWeb3Provider.cs b/Explorer/Assets/DCL/Web3/Authenticators/ICompositeWeb3Provider.cs index 4abf420874d..045828076e4 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/ICompositeWeb3Provider.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/ICompositeWeb3Provider.cs @@ -10,10 +10,10 @@ namespace DCL.Web3.Authenticators /// /// Interface for composite authentication provider that supports multiple authentication methods. - /// Combines base authentication, Ethereum API, Dapp verification, and OTP flows. + /// Combines base authentication, Ethereum API, and OTP flows. /// This is the single entry point for all Web3 authentication needs. /// - public interface ICompositeWeb3Provider : IWeb3Authenticator, IEthereumApi, IDappVerificationHandler, IOtpAuthenticator + public interface ICompositeWeb3Provider : IWeb3Authenticator, IEthereumApi, IOtpAuthenticator { /// /// Currently selected authentication method diff --git a/Explorer/Assets/DCL/Web3/Authenticators/IDappVerificationHandler.cs b/Explorer/Assets/DCL/Web3/Authenticators/IDappVerificationHandler.cs deleted file mode 100644 index 6bb62f9ba50..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/IDappVerificationHandler.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace DCL.Web3.Authenticators -{ - /// - /// Interface for handling Dapp wallet verification flow. - /// Used when user authenticates via browser wallet (MetaMask, WalletConnect, etc.) - /// - public interface IDappVerificationHandler - { - /// - /// Raised when verification code should be displayed to the user. - /// Always invoked on the main thread. - /// - public event Action<(int code, DateTime expiration, string requestId)>? VerificationRequired; - } -} diff --git a/Explorer/Assets/DCL/Web3/Authenticators/IDappVerificationHandler.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/IDappVerificationHandler.cs.meta deleted file mode 100644 index 3c1b37e3e46..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/IDappVerificationHandler.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 447b69cf9ca1a3944be0bff61fc3de4f \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs index e18c5e7db50..63a9ff2f434 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs @@ -11,8 +11,8 @@ namespace DCL.Web3.Authenticators /// /// Composite provider that wraps both authentication methods (ThirdWeb OTP and Dapp Wallet) /// and delegates calls to the currently selected method. - /// Implements ICompositeWeb3Provider which combines IWeb3Authenticator, IEthereumApi, - /// IDappVerificationHandler and IOtpAuthenticator to provide a single entry point for all Web3 needs. + /// Implements ICompositeWeb3Provider which combines IWeb3Authenticator, IEthereumApi + /// and IOtpAuthenticator to provide a single entry point for all Web3 needs. /// public class CompositeWeb3Provider : ICompositeWeb3Provider { @@ -24,13 +24,6 @@ public class CompositeWeb3Provider : ICompositeWeb3Provider public AuthProvider CurrentProvider { private get; set; } = AuthProvider.Dapp; - // IDappVerificationHandler - delegates to dappAuth - public event Action<(int code, DateTime expiration, string requestId)>? VerificationRequired - { - add => dappAuth.VerificationRequired += value; - remove => dappAuth.VerificationRequired -= value; - } - public event Action? OTPSendSucceeded { add => thirdWebAuth.OTPSendSucceeded += value; diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Default.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Default.cs index f26dc0b0b40..6d145ff2611 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Default.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Default.cs @@ -4,7 +4,6 @@ using DCL.Multiplayer.Connections.DecentralandUrls; using DCL.Web3.Abstract; using DCL.Web3.Identities; -using System; using System.Collections.Generic; using System.Threading; @@ -12,16 +11,10 @@ namespace DCL.Web3.Authenticators { public partial class DappWeb3Authenticator { - public class Default : IWeb3Authenticator, IEthereumApi, IDappVerificationHandler + public class Default : IWeb3Authenticator, IEthereumApi { private readonly DappWeb3Authenticator origin; - public event Action<(int code, DateTime expiration, string requestId)>? VerificationRequired - { - add => origin.VerificationRequired += value; - remove => origin.VerificationRequired -= value; - } - public Default(IWeb3IdentityCache identityCache, IDecentralandUrlsSource decentralandUrlsSource, IWeb3AccountFactory web3AccountFactory, DecentralandEnvironment environment) { URLAddress authApiUrl = URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.ApiAuth)); @@ -58,8 +51,7 @@ public Default(IWeb3IdentityCache identityCache, IDecentralandUrlsSource decentr "eth_getTransactionCount", "eth_getBlockByNumber", "eth_getCode" }, - environment, - new InvalidAuthCodeVerificationFeatureFlag() + environment ); } @@ -76,15 +68,6 @@ public UniTask LoginAsync(LoginPayload payload, CancellationToken public UniTask LogoutAsync(CancellationToken ct) => origin.LogoutAsync(ct); - - // IDappVerificationHandler - public void CancelCurrentWeb3Operation() => - origin.CancelCurrentWeb3Operation(); - - private class InvalidAuthCodeVerificationFeatureFlag : ICodeVerificationFeatureFlag - { - public bool ShouldWaitForCodeVerificationFromServer => false; - } } } } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.ICodeVerificationFeatureFlag.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.ICodeVerificationFeatureFlag.cs deleted file mode 100644 index 3b768c49f3c..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.ICodeVerificationFeatureFlag.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DCL.Web3.Authenticators -{ - public partial class DappWeb3Authenticator - { - public interface ICodeVerificationFeatureFlag - { - public bool ShouldWaitForCodeVerificationFromServer { get; } - } - } -} diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.ICodeVerificationFeatureFlag.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.ICodeVerificationFeatureFlag.cs.meta deleted file mode 100644 index de30f6f1ca2..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.ICodeVerificationFeatureFlag.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c1518914ba266434a9280117d00451ea \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.VerificationStatus.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.VerificationStatus.cs deleted file mode 100644 index 8699a10d4e8..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.VerificationStatus.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace DCL.Web3.Authenticators -{ - public partial class DappWeb3Authenticator - { - [Serializable] - private struct CodeVerificationStatus - { - public string requestId; - } - } -} diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.VerificationStatus.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.VerificationStatus.cs.meta deleted file mode 100644 index 1a9f2065ec8..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.VerificationStatus.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 5c9ce2ca4b793fd45b2b0af94c16b7d5 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs index 58c56e6460b..38bc0ea7a30 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs @@ -20,7 +20,7 @@ namespace DCL.Web3.Authenticators { - public partial class DappWeb3Authenticator : IWeb3Authenticator, IEthereumApi, IDappVerificationHandler + public partial class DappWeb3Authenticator : IWeb3Authenticator, IEthereumApi { private const double IDENTITY_EXPIRATION_PERIOD = 30; @@ -36,7 +36,6 @@ public partial class DappWeb3Authenticator : IWeb3Authenticator, IEthereumApi, I private readonly HashSet whitelistMethods; private readonly HashSet readOnlyMethods; private readonly DecentralandEnvironment environment; - private readonly ICodeVerificationFeatureFlag codeVerificationFeatureFlag; private readonly int? identityExpirationDuration; // Allow only one web3 operation at a time @@ -49,12 +48,6 @@ public partial class DappWeb3Authenticator : IWeb3Authenticator, IEthereumApi, I private SocketIO? authApiWebSocket; private DCLWebSocket? rpcWebSocket; private UniTaskCompletionSource? signatureOutcomeTask; - private UniTaskCompletionSource? codeVerificationTask; - - public delegate void VerificationDelegate(int code, DateTime expiration); - private VerificationDelegate? signatureVerificationCallback; - - public event Action<(int code, DateTime expiration, string requestId)>? VerificationRequired; public DappWeb3Authenticator(IWebBrowser webBrowser, URLAddress authApiUrl, @@ -64,7 +57,7 @@ public DappWeb3Authenticator(IWebBrowser webBrowser, IWeb3AccountFactory web3AccountFactory, HashSet whitelistMethods, HashSet readOnlyMethods, - DecentralandEnvironment environment, ICodeVerificationFeatureFlag codeVerificationFeatureFlag, + DecentralandEnvironment environment, int? identityExpirationDuration = null) { this.webBrowser = webBrowser; @@ -76,7 +69,6 @@ public DappWeb3Authenticator(IWebBrowser webBrowser, this.whitelistMethods = whitelistMethods; this.readOnlyMethods = readOnlyMethods; this.environment = environment; - this.codeVerificationFeatureFlag = codeVerificationFeatureFlag; this.identityExpirationDuration = identityExpirationDuration; } @@ -140,17 +132,16 @@ public async UniTask SendAsync(EthApiRequest request, Web3Reques return await SendWithConfirmationAsync(request, ct); } - public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) => - await LoginAsync2(payload, ct); - /// - /// 1. An authentication request is sent to the server - /// 2. Open a tab to let the user sign through the browser with his custom installed wallet - /// 3. Use the signature information to generate the identity + /// Legacy socket-based wallet sign-in (kept for the Archipelago dev playgrounds via + /// ). The production wallet flow is . + /// 1. An authentication request is sent to the server. + /// 2. Open a tab to let the user sign through the browser with his custom installed wallet. + /// 3. Use the signature information to generate the identity. /// /// Login payload containing the authentication method /// - public async UniTask LoginAsync2(LoginPayload payload, CancellationToken ct) + public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) { await mutex.WaitAsync(ct); @@ -186,11 +177,6 @@ public async UniTask LoginAsync2(LoginPayload payload, Cancellati await UniTask.SwitchToMainThread(ct); - if (codeVerificationFeatureFlag.ShouldWaitForCodeVerificationFromServer) - WaitForCodeVerificationAsync(authenticationResponse.requestId, authenticationResponse.code, signatureExpiration, ct).Forget(); - else - VerificationRequired?.Invoke((authenticationResponse.code, signatureExpiration, authenticationResponse.requestId)); - LoginAuthApiResponse response = await RequestSignatureAsync(authenticationResponse.requestId, payload.Method, signatureExpiration, ct); @@ -232,21 +218,11 @@ public async UniTask LoginAsync2(LoginPayload payload, Cancellati public async UniTask LogoutAsync(CancellationToken ct) => await DisconnectFromAuthApiAsync(); - public void CancelCurrentWeb3Operation() - { - // Cancel the task waiting for the browser signature - signatureOutcomeTask?.TrySetCanceled(); - - // Also cancel code verification if that's what was hanging (during Login) - codeVerificationTask?.TrySetCanceled(); - } - private async UniTask DisconnectFromAuthApiAsync() { if (authApiWebSocket is { Connected: true }) await authApiWebSocket.DisconnectAsync(); - codeVerificationTask?.TrySetCanceled(); signatureOutcomeTask?.TrySetCanceled(); } @@ -378,8 +354,6 @@ private async UniTask SendWithConfirmationAsync(EthApiRequest re await UniTask.SwitchToMainThread(ct); - signatureVerificationCallback?.Invoke(authenticationResponse.code, signatureExpiration); - MethodResponse response = await RequestSignatureAsync(authenticationResponse.requestId, null, signatureExpiration, ct); if (authApiPendingOperations <= 1) @@ -446,9 +420,6 @@ private string CreateEphemeralMessage(IWeb3Account ephemeralAccount, DateTime ex private void ProcessSignatureOutcomeMessage(SocketIOResponse response) => signatureOutcomeTask?.TrySetResult(response); - private void ProcessCodeVerificationStatus(SocketIOResponse response) => - codeVerificationTask?.TrySetResult(response); - private async UniTask RequestSignatureAsync(string requestId, LoginMethod? method, DateTime expiration, CancellationToken ct) { string url = @@ -509,7 +480,6 @@ private async UniTask ConnectToAuthApiAsync() authApiWebSocket.JsonSerializer = new NewtonsoftJsonSerializer(new JsonSerializerSettings()); authApiWebSocket.On("outcome", ProcessSignatureOutcomeMessage); - authApiWebSocket.On("request-validation-status", ProcessCodeVerificationStatus); authApiWebSocket.OnDisconnected += OnWebSocketDisconnected; authApiWebSocket.OnError += OnWebSocketError; } @@ -524,13 +494,11 @@ await authApiWebSocket private void OnWebSocketError(object sender, string error) { signatureOutcomeTask?.TrySetException(new WebSocketException(WebSocketError.Faulted, error)); - codeVerificationTask?.TrySetException(new WebSocketException(WebSocketError.Faulted, error)); } private void OnWebSocketDisconnected(object sender, string reason) { signatureOutcomeTask?.TrySetException(new WebSocketException(WebSocketError.ConnectionClosedPrematurely, reason)); - codeVerificationTask?.TrySetException(new WebSocketException(WebSocketError.ConnectionClosedPrematurely, reason)); } private bool IsReadOnly(EthApiRequest request) @@ -541,32 +509,5 @@ private bool IsReadOnly(EthApiRequest request) return false; } - - /// - /// Waits until we receive the verification status from the server - /// So then we raise the VerificationRequired event - /// - private async UniTask WaitForCodeVerificationAsync(string requestId, int code, DateTime expiration, CancellationToken ct) - { - codeVerificationTask?.TrySetCanceled(ct); - codeVerificationTask = new UniTaskCompletionSource(); - - TimeSpan duration = expiration - DateTime.UtcNow; - - try - { - SocketIOResponse response = await codeVerificationTask.Task.Timeout(duration).AttachExternalCancellation(ct); - - CodeVerificationStatus validation = response.GetValue(); - - if (validation.requestId == requestId) - { - await UniTask.SwitchToMainThread(ct); - VerificationRequired?.Invoke((code, expiration, requestId)); - } - } - catch (TimeoutException e) { throw new CodeVerificationException($"Code verification expired: {expiration}", e); } - catch (WebSocketException e) { throw new CodeVerificationException("An error occurred while verifying the code: unable to complete the operation due to a WebSocket issue", e); } - } } } From add0163e95541484e193b26af9721a8e79547de6 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 13:16:32 +0200 Subject: [PATCH 11/54] removing bridge file; -logs cleanup --- .../DeepLinkHandleImplementation.cs | 6 ---- .../DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 35 ++++--------------- .../DeeplinkSigninDispatcher.cs | 5 --- 3 files changed, 6 insertions(+), 40 deletions(-) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index fd0f17bbe1a..9128a60e5ef 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -35,19 +35,13 @@ public bool HandleDeepLink(DeepLink deeplink) // community notification is triggered. string? signin = deeplink.ValueOf(AppArgsFlags.SIGNIN); - Debug.Log($"[DLDBG] HandleDeepLink received: {deeplink} | extracted signin='{signin}'"); // TODO: temporary deep-link debug log, remove. - if (!string.IsNullOrEmpty(signin)) { // Only consume the signin when a login flow is actively awaiting it; otherwise leave the bridge // file in place so the instance that is logging in can pick it up. if (!deeplinkSigninDispatcher.HasSubscriber) - { - Debug.Log("[DLDBG] HandleDeepLink deferring signin: no subscriber, leaving bridge file"); // TODO: temporary deep-link debug log, remove. return false; - } - Debug.Log($"[DLDBG] HandleDeepLink dispatching signin='{signin}'"); // TODO: temporary deep-link debug log, remove. deeplinkSigninDispatcher.Dispatch(signin); ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"{Name} dispatched signin deeplink: {deeplink}"); return true; diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index dcebda3e61e..1537fdcc219 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -5,7 +5,6 @@ using System; using System.IO; using System.Threading; -using UnityEngine; namespace DCL.RuntimeDeepLink { @@ -36,8 +35,6 @@ public static class DeepLinkSentinel /// public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandle handle, CancellationToken token) { - Debug.Log($"[DLDBG] Sentinel STARTED . Watching: {DEEP_LINK_BRIDGE_PATH}"); // TODO: temporary deep-link debug log, remove. - while (token.IsCancellationRequested == false) { bool cancelled = await UniTask.Delay(CHECK_IN_PERIOD, cancellationToken: token).SuppressCancellationThrow(); @@ -46,57 +43,37 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl // File.Exists method is lightweight and can be used in this loop if (!File.Exists(DEEP_LINK_BRIDGE_PATH)) continue; - // TODO: temporary deep-link debug log, remove. FileInfo.Exists is checked to avoid throwing if the - // file vanished between the check above and reading its metadata (launcher rewrite / other instance). - var bridgeInfo = new FileInfo(DEEP_LINK_BRIDGE_PATH); - if (bridgeInfo.Exists) - Debug.Log($"[DLDBG] Sentinel: bridge file FOUND | created={bridgeInfo.CreationTimeUtc:HH:mm:ss.fff}Z written={bridgeInfo.LastWriteTimeUtc:HH:mm:ss.fff}Z size={bridgeInfo.Length}B path={DEEP_LINK_BRIDGE_PATH}"); - Result contentResult = await File.ReadAllTextAsync(DEEP_LINK_BRIDGE_PATH, token)!.SuppressToResultAsync(ReportCategory.RUNTIME_DEEPLINKS); // Transient IO read failure: leave the file for the next check-in. if (contentResult.Success == false) continue; - Debug.Log($"[DLDBG] Sentinel: raw content = {contentResult.Value}"); // TODO: temporary deep-link debug log, remove. - // Parse before deleting: a corrupt file is dropped, a valid one is handled. Result deepLinkCreateResult = DeepLink.FromJson(contentResult.Value); if (deepLinkCreateResult.Success == false) { ReportHub.LogError(ReportCategory.RUNTIME_DEEPLINKS, $"Cannot deserialize deeplink content: {deepLinkCreateResult.ErrorMessage}"); - TryArchiveBridgeFile(); + TryDeleteBridgeFile(); continue; } // A false result means no login flow is awaiting the signin yet: keep the file so it can be picked up later. if (handle.HandleDeepLink(deepLinkCreateResult.Value)) - { - Debug.Log("[DLDBG] Sentinel: deeplink consumed, archiving bridge file"); // TODO: temporary deep-link debug log, remove. - TryArchiveBridgeFile(); - } - else - Debug.Log("[DLDBG] Sentinel: deeplink deferred, keeping bridge file"); // TODO: temporary deep-link debug log, remove. + TryDeleteBridgeFile(); } } - // TODO: temporary deep-link debug. Instead of deleting, rename the consumed bridge file with the consuming - // process PID so the traces stay on disk for inspection. Restore File.Delete once the flow is understood. - private static void TryArchiveBridgeFile() + private static void TryDeleteBridgeFile() { try { - int pid = System.Diagnostics.Process.GetCurrentProcess().Id; - string directory = Path.GetDirectoryName(DEEP_LINK_BRIDGE_PATH)!; - string archivedPath = Path.Combine(directory, $"deeplink-bridge.consumed.pid{pid}.{DateTime.UtcNow:yyyyMMdd-HHmmss-fff}.json"); - - File.Move(DEEP_LINK_BRIDGE_PATH, archivedPath); - Debug.Log($"[DLDBG] Sentinel: archived bridge file -> {archivedPath}"); // TODO: temporary deep-link debug log, remove. + File.Delete(DEEP_LINK_BRIDGE_PATH); } catch (Exception e) { - // Rename can fail transiently (file locked/rewritten by the launcher). Log and keep the loop alive. - ReportHub.LogError(ReportCategory.RUNTIME_DEEPLINKS, $"Failed to archive deeplink bridge file: {e.Message}"); + // Delete can fail transiently (file locked/rewritten by the launcher). Log and keep the loop alive. + ReportHub.LogError(ReportCategory.RUNTIME_DEEPLINKS, $"Failed to delete deeplink bridge file: {e.Message}"); } } } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs index 716ebad5b2d..90252365444 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs @@ -1,5 +1,4 @@ using System; -using UnityEngine; namespace DCL.RuntimeDeepLink { @@ -12,8 +11,6 @@ public class DeeplinkSigninDispatcher : IDeeplinkSigninDispatcher public IDisposable Subscribe(Action onSigninReceived, string? expectedRequestId = null) { - Debug.Log("[DLDBG] Dispatcher.Subscribe"); // TODO: temporary deep-link debug log, remove. - var subscription = new Subscription(this, onSigninReceived); current = subscription; return subscription; @@ -21,8 +18,6 @@ public IDisposable Subscribe(Action onSigninReceived, string? expectedRe public void Dispatch(string identityId, string? sourceRequestId = null) { - Debug.Log($"[DLDBG] Dispatcher.Dispatch id='{identityId}' hasSubscriber={current != null}"); // TODO: temporary deep-link debug log, remove. - // Stage 1: the sourceRequestId / expectedRequestId correlation is intentionally not yet applied. current?.Handler(identityId); } From abf574e86bcdc0aee149c393edf29ad645cc5231 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 13:59:48 +0200 Subject: [PATCH 12/54] renaming of old Dapp auth --- .../AuthenticationScreenController.cs | 2 +- ...entityVerificationDappDeepLinkAuthState.cs | 6 ++++ .../IdentityVerificationOTPAuthState.cs | 16 +++++----- .../Global/Dynamic/BootstrapContainer.cs | 29 +++++++------------ .../Global/Dynamic/DynamicWorldContainer.cs | 4 +-- .../Demo/ArchipelagoFakeIdentityCache.cs | 2 +- .../Implementations/CompositeWeb3Provider.cs | 4 +-- ...eb3Authenticator.TransactionApiResponse.cs | 13 --------- ...thenticator.TransactionApiResponse.cs.meta | 2 -- ...eb3Authenticator.TransferAuthApiRequest.cs | 14 --------- ...thenticator.TransferAuthApiRequest.cs.meta | 2 -- ...ault.cs => DappWeb3EthereumApi.Default.cs} | 6 ++-- ...ta => DappWeb3EthereumApi.Default.cs.meta} | 0 ...t.cs => DappWeb3EthereumApi.EthRequest.cs} | 2 +- ...=> DappWeb3EthereumApi.EthRequest.cs.meta} | 0 ...appWeb3EthereumApi.LoginAuthApiRequest.cs} | 2 +- ...b3EthereumApi.LoginAuthApiRequest.cs.meta} | 0 ...ppWeb3EthereumApi.LoginAuthApiResponse.cs} | 2 +- ...3EthereumApi.LoginAuthApiResponse.cs.meta} | 0 ... => DappWeb3EthereumApi.MethodResponse.cs} | 2 +- ...appWeb3EthereumApi.MethodResponse.cs.meta} | 0 ...appWeb3EthereumApi.SignatureIdResponse.cs} | 2 +- ...b3EthereumApi.SignatureIdResponse.cs.meta} | 0 ...uthenticator.cs => DappWeb3EthereumApi.cs} | 4 +-- ...or.cs.meta => DappWeb3EthereumApi.cs.meta} | 0 25 files changed, 40 insertions(+), 74 deletions(-) delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransactionApiResponse.cs delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransactionApiResponse.cs.meta delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransferAuthApiRequest.cs delete mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransferAuthApiRequest.cs.meta rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.Default.cs => DappWeb3EthereumApi.Default.cs} (94%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.Default.cs.meta => DappWeb3EthereumApi.Default.cs.meta} (100%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.EthRequest.cs => DappWeb3EthereumApi.EthRequest.cs} (86%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.EthRequest.cs.meta => DappWeb3EthereumApi.EthRequest.cs.meta} (100%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.LoginAuthApiRequest.cs => DappWeb3EthereumApi.LoginAuthApiRequest.cs} (82%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.LoginAuthApiRequest.cs.meta => DappWeb3EthereumApi.LoginAuthApiRequest.cs.meta} (100%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.LoginAuthApiResponse.cs => DappWeb3EthereumApi.LoginAuthApiResponse.cs} (84%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.LoginAuthApiResponse.cs.meta => DappWeb3EthereumApi.LoginAuthApiResponse.cs.meta} (100%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.MethodResponse.cs => DappWeb3EthereumApi.MethodResponse.cs} (84%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.MethodResponse.cs.meta => DappWeb3EthereumApi.MethodResponse.cs.meta} (100%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.SignatureIdResponse.cs => DappWeb3EthereumApi.SignatureIdResponse.cs} (86%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.SignatureIdResponse.cs.meta => DappWeb3EthereumApi.SignatureIdResponse.cs.meta} (100%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.cs => DappWeb3EthereumApi.cs} (99%) rename Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/{DappWeb3Authenticator.cs.meta => DappWeb3EthereumApi.cs.meta} (100%) diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs index 7dff4a6fb2f..ac5e0d2f811 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs @@ -165,7 +165,7 @@ protected override void OnViewInstantiated() new InitAuthState(viewInstance, installSource), new LoginSelectionAuthState(fsm, viewInstance, this, CurrentState, splashScreen, web3Authenticator, webBrowser, enableEmailOTP), new ProfileFetchingAuthState(fsm, viewInstance, this, CurrentState, selfProfile, storedIdentityProvider), - new IdentityVerificationDappDeepLinkAuthState(fsm, viewInstance, this, web3Authenticator), + new IdentityVerificationDappDeepLinkAuthState(fsm, viewInstance, this, CurrentState, web3Authenticator), new LobbyForExistingAccountAuthState(fsm, viewInstance, this, splashScreen, CurrentState, characterPreviewController), new LobbyForNewAccountAuthState(fsm, viewInstance, this, CurrentState, characterPreviewController, selfProfile, wearablesProvider, webBrowser, webRequestController, decentralandUrlsSource, profileChangesBus) ); diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs index a6b2d87ff48..44563060835 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs @@ -1,5 +1,6 @@ using Cysharp.Threading.Tasks; using DCL.Diagnostics; +using DCL.Utilities; using DCL.Web3; using DCL.Web3.Authenticators; using DCL.Web3.Identities; @@ -7,6 +8,7 @@ using Plugins.NativeWindowManager; using System; using System.Threading; +using static DCL.AuthenticationScreenFlow.AuthenticationScreenController; using static DCL.UI.UIAnimationHashes; namespace DCL.AuthenticationScreenFlow @@ -15,6 +17,7 @@ public class IdentityVerificationDappDeepLinkAuthState : AuthStateBase, IPayload { private readonly MVCStateMachine machine; private readonly AuthenticationScreenController controller; + private readonly ReactiveProperty currentState; private readonly ICompositeWeb3Provider compositeWeb3Provider; private Exception? loginException; @@ -23,10 +26,12 @@ public IdentityVerificationDappDeepLinkAuthState( MVCStateMachine machine, AuthenticationScreenView viewInstance, AuthenticationScreenController controller, + ReactiveProperty currentState, ICompositeWeb3Provider compositeWeb3Provider) : base(viewInstance) { this.machine = machine; this.controller = controller; + this.currentState = currentState; this.compositeWeb3Provider = compositeWeb3Provider; } @@ -41,6 +46,7 @@ public void Enter((LoginMethod method, CancellationToken ct) payload) controller.CurrentRequestID = string.Empty; + currentState.Value = AuthStatus.VerificationRequested; AuthenticateAsync(payload.method, payload.ct).Forget(); } diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationOTPAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationOTPAuthState.cs index 0f9360fa542..96f43db1176 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationOTPAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationOTPAuthState.cs @@ -66,14 +66,14 @@ public override void Exit() view.Hide(SLIDE); spanErrorInfo = loginException switch - { - OperationCanceledException => new SpanErrorInfo("Login process was cancelled by user"), - SignatureExpiredException ex => new SpanErrorInfo("Web3 signature expired during authentication", ex), - Web3SignatureException ex => new SpanErrorInfo("Web3 signature validation failed", ex), - CodeVerificationException ex => new SpanErrorInfo("Code verification failed during authentication", ex), - InvalidEmailException ex => new SpanErrorInfo("Invalid email provided during authentication", ex), - Exception ex => new SpanErrorInfo("Unexpected error during authentication flow", ex), - }; + { + OperationCanceledException => new SpanErrorInfo("Login process was cancelled by user"), + SignatureExpiredException ex => new SpanErrorInfo("Web3 signature expired during authentication", ex), + Web3SignatureException ex => new SpanErrorInfo("Web3 signature validation failed", ex), + CodeVerificationException ex => new SpanErrorInfo("Code verification failed during authentication", ex), + InvalidEmailException ex => new SpanErrorInfo("Invalid email provided during authentication", ex), + Exception ex => new SpanErrorInfo("Unexpected error during authentication flow", ex), + }; if (loginException is not OperationCanceledException && loginException is not InvalidEmailException) ReportHub.LogException(loginException, new ReportData(ReportCategory.AUTHENTICATION)); diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index 1e91ab69a54..76cf640c301 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -49,13 +49,7 @@ public class BootstrapContainer : DCLGlobalContainer public IBootstrap? Bootstrap { get; private set; } public IWeb3IdentityCache? IdentityCache { get; private set; } public ICompositeWeb3Provider? CompositeWeb3Provider { get; private set; } - - /// - /// Shared single-slot signin deep-link bus. Created here (the first container, before any consumer) so the - /// SAME instance is injected into both the Dapp authenticator (which awaits it during the deep-link login) - /// and the runtime DeepLinkHandle (which dispatches into it). - /// - public IDeeplinkSigninDispatcher DeeplinkSigninDispatcher { get; private set; } + public IDeeplinkSigninDispatcher DeeplinkSigninDispatcher { get; private set; } // Shared single-slot signin deep-link bus. public AnalyticsContainer Analytics { get; private set; } public DebugSettings.DebugSettings DebugSettings { get; private set; } public VolumeBus VolumeBus { get; private set; } @@ -211,29 +205,26 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies( identityExpirationDuration ); - // Create Dapp authenticator (Browser wallet) - var dappAuth = new DappWeb3Authenticator( + var dappDeepLinkAuth = new DappDeepLinkAuthenticator( webBrowser, URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.ApiAuth)), URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.AuthSignatureWebApp)), - URLDomain.FromString(decentralandUrlsSource.Url(DecentralandUrl.ApiRpc)), - identityCache, web3AccountFactory, - new HashSet(sceneLoaderSettings.Web3WhitelistMethods), - new HashSet(sceneLoaderSettings.Web3ReadOnlyMethods), - dclEnvironment, + webRequestController, + deeplinkSigninDispatcher, identityExpirationDuration ); - // Deep-link sign-in is a self-contained authenticator; the socket-based DappWeb3Authenticator above stays - // responsible for the IEthereumApi signature/confirmation flow only. - var dappDeepLinkAuth = new DappDeepLinkAuthenticator( + var dappAuth = new DappWeb3EthereumApi( webBrowser, URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.ApiAuth)), URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.AuthSignatureWebApp)), + URLDomain.FromString(decentralandUrlsSource.Url(DecentralandUrl.ApiRpc)), + identityCache, web3AccountFactory, - webRequestController, - deeplinkSigninDispatcher, + new HashSet(sceneLoaderSettings.Web3WhitelistMethods), + new HashSet(sceneLoaderSettings.Web3ReadOnlyMethods), + dclEnvironment, identityExpirationDuration ); diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs index 01eab8d5d9e..956aa42d49f 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -475,8 +475,8 @@ await MapRendererContainer // Local scene development scenes are excluded from deeplink runtime handling logic if (!appArgs.HasFlag(AppArgsFlags.LOCAL_SCENE)) { - // Shared with the Dapp authenticator's deep-link login (created in BootstrapContainer): the browser - // delivers the signin deep link here, DeepLinkHandle dispatches it, and DappWeb3Authenticator awaits it. + // Shared with the deep-link login (created in BootstrapContainer): the browser delivers the signin + // deep link here, DeepLinkHandle dispatches it, and DappDeepLinkAuthenticator awaits it. var deepLinkHandleImplementation = new DeepLinkHandle(dynamicWorldParams.StartParcel, chatContainer.ChatTeleporter, ct, communitiesDataService, bootstrapContainer.DeeplinkSigninDispatcher); deepLinkHandleImplementation.StartListenForDeepLinksAsync(ct).Forget(); } diff --git a/Explorer/Assets/DCL/Multiplayer/Connections/Demo/ArchipelagoFakeIdentityCache.cs b/Explorer/Assets/DCL/Multiplayer/Connections/Demo/ArchipelagoFakeIdentityCache.cs index 0778e9e205f..fd92918eb25 100644 --- a/Explorer/Assets/DCL/Multiplayer/Connections/Demo/ArchipelagoFakeIdentityCache.cs +++ b/Explorer/Assets/DCL/Multiplayer/Connections/Demo/ArchipelagoFakeIdentityCache.cs @@ -27,7 +27,7 @@ DecentralandEnvironment dclEnvironment if (identityCache.Identity is null) { - IWeb3Identity? identity = await new DappWeb3Authenticator.Default(identityCache, decentralandUrlsSource, web3AccountFactory, dclEnvironment) + IWeb3Identity? identity = await new DappWeb3EthereumApi.Default(identityCache, decentralandUrlsSource, web3AccountFactory, dclEnvironment) .LoginAsync(LoginPayload.ForDappFlow(LoginMethod.ANY), CancellationToken.None); identityCache.Identity = identity; diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs index 63a9ff2f434..d048816e993 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs @@ -17,7 +17,7 @@ namespace DCL.Web3.Authenticators public class CompositeWeb3Provider : ICompositeWeb3Provider { private readonly ThirdWebAuthenticator thirdWebAuth; - private readonly DappWeb3Authenticator dappAuth; + private readonly DappWeb3EthereumApi dappAuth; private readonly IWeb3Authenticator dappLogin; private readonly IWeb3IdentityCache identityCache; private readonly IAnalyticsController analytics; @@ -37,7 +37,7 @@ public event Action? OTPSendSucceeded public CompositeWeb3Provider( ThirdWebAuthenticator thirdWebAuth, - DappWeb3Authenticator dappAuth, + DappWeb3EthereumApi dappAuth, DappDeepLinkAuthenticator dappLogin, IWeb3IdentityCache identityCache, IAnalyticsController analytics) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransactionApiResponse.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransactionApiResponse.cs deleted file mode 100644 index 12138998f46..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransactionApiResponse.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace DCL.Web3.Authenticators -{ - public partial class DappWeb3Authenticator - { - [Serializable] - public struct TransactionApiResponse - { - public string txHash; - } - } -} \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransactionApiResponse.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransactionApiResponse.cs.meta deleted file mode 100644 index 90bcad5db13..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransactionApiResponse.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c1bc092dc90b18d4c9a88ef720128422 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransferAuthApiRequest.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransferAuthApiRequest.cs deleted file mode 100644 index 24fddd54323..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransferAuthApiRequest.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace DCL.Web3.Authenticators -{ - public partial class DappWeb3Authenticator - { - [Serializable] - public struct TransferAuthApiRequest - { - public string method; - public object[] @params; - } - } -} \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransferAuthApiRequest.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransferAuthApiRequest.cs.meta deleted file mode 100644 index dbfdec40154..00000000000 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.TransferAuthApiRequest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: dc75ab5ffcbd30743abf601ea5a2ead4 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Default.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.Default.cs similarity index 94% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Default.cs rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.Default.cs index 6d145ff2611..cd0f55b6ec0 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Default.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.Default.cs @@ -9,11 +9,11 @@ namespace DCL.Web3.Authenticators { - public partial class DappWeb3Authenticator + public partial class DappWeb3EthereumApi { public class Default : IWeb3Authenticator, IEthereumApi { - private readonly DappWeb3Authenticator origin; + private readonly DappWeb3EthereumApi origin; public Default(IWeb3IdentityCache identityCache, IDecentralandUrlsSource decentralandUrlsSource, IWeb3AccountFactory web3AccountFactory, DecentralandEnvironment environment) { @@ -21,7 +21,7 @@ public Default(IWeb3IdentityCache identityCache, IDecentralandUrlsSource decentr URLAddress signatureUrl = URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.AuthSignatureWebApp)); URLDomain rpcServerUrl = URLDomain.FromString(decentralandUrlsSource.Url(DecentralandUrl.ApiRpc)); - origin = new DappWeb3Authenticator( + origin = new DappWeb3EthereumApi( new UnityAppWebBrowser(decentralandUrlsSource), authApiUrl, signatureUrl, diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Default.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.Default.cs.meta similarity index 100% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.Default.cs.meta rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.Default.cs.meta diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.EthRequest.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.EthRequest.cs similarity index 86% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.EthRequest.cs rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.EthRequest.cs index 75d68a581d3..f9f8d43fdce 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.EthRequest.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.EthRequest.cs @@ -3,7 +3,7 @@ namespace DCL.Web3.Authenticators { - public partial class DappWeb3Authenticator + public partial class DappWeb3EthereumApi { [Serializable] private struct AuthorizedEthApiRequest diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.EthRequest.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.EthRequest.cs.meta similarity index 100% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.EthRequest.cs.meta rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.EthRequest.cs.meta diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.LoginAuthApiRequest.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.LoginAuthApiRequest.cs similarity index 82% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.LoginAuthApiRequest.cs rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.LoginAuthApiRequest.cs index 2dfde7a57f3..2c8e4dd91f7 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.LoginAuthApiRequest.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.LoginAuthApiRequest.cs @@ -2,7 +2,7 @@ namespace DCL.Web3.Authenticators { - public partial class DappWeb3Authenticator + public partial class DappWeb3EthereumApi { [Serializable] public struct LoginAuthApiRequest diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.LoginAuthApiRequest.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.LoginAuthApiRequest.cs.meta similarity index 100% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.LoginAuthApiRequest.cs.meta rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.LoginAuthApiRequest.cs.meta diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.LoginAuthApiResponse.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.LoginAuthApiResponse.cs similarity index 84% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.LoginAuthApiResponse.cs rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.LoginAuthApiResponse.cs index dfbc361d06f..ce21e5ea0d0 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.LoginAuthApiResponse.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.LoginAuthApiResponse.cs @@ -2,7 +2,7 @@ namespace DCL.Web3.Authenticators { - public partial class DappWeb3Authenticator + public partial class DappWeb3EthereumApi { [Serializable] private struct LoginAuthApiResponse diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.LoginAuthApiResponse.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.LoginAuthApiResponse.cs.meta similarity index 100% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.LoginAuthApiResponse.cs.meta rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.LoginAuthApiResponse.cs.meta diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.MethodResponse.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.MethodResponse.cs similarity index 84% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.MethodResponse.cs rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.MethodResponse.cs index 446681d4940..ba503b91070 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.MethodResponse.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.MethodResponse.cs @@ -2,7 +2,7 @@ namespace DCL.Web3.Authenticators { - public partial class DappWeb3Authenticator + public partial class DappWeb3EthereumApi { [Serializable] private struct MethodResponse diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.MethodResponse.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.MethodResponse.cs.meta similarity index 100% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.MethodResponse.cs.meta rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.MethodResponse.cs.meta diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.SignatureIdResponse.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.SignatureIdResponse.cs similarity index 86% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.SignatureIdResponse.cs rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.SignatureIdResponse.cs index 42522810093..f102ff91c20 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.SignatureIdResponse.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.SignatureIdResponse.cs @@ -2,7 +2,7 @@ namespace DCL.Web3.Authenticators { - public partial class DappWeb3Authenticator + public partial class DappWeb3EthereumApi { [Serializable] private struct SignatureIdResponse diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.SignatureIdResponse.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.SignatureIdResponse.cs.meta similarity index 100% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.SignatureIdResponse.cs.meta rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.SignatureIdResponse.cs.meta diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs similarity index 99% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs index 38bc0ea7a30..b08b9fbf8f9 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs @@ -20,7 +20,7 @@ namespace DCL.Web3.Authenticators { - public partial class DappWeb3Authenticator : IWeb3Authenticator, IEthereumApi + public partial class DappWeb3EthereumApi : IEthereumApi { private const double IDENTITY_EXPIRATION_PERIOD = 30; @@ -49,7 +49,7 @@ public partial class DappWeb3Authenticator : IWeb3Authenticator, IEthereumApi private DCLWebSocket? rpcWebSocket; private UniTaskCompletionSource? signatureOutcomeTask; - public DappWeb3Authenticator(IWebBrowser webBrowser, + public DappWeb3EthereumApi(IWebBrowser webBrowser, URLAddress authApiUrl, URLAddress signatureWebAppUrl, URLDomain rpcServerUrl, diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs.meta similarity index 100% rename from Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3Authenticator.cs.meta rename to Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs.meta From 93950b15429daab3472bbf169d11fbda899d4b15 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 14:03:12 +0200 Subject: [PATCH 13/54] spacing --- .../IdentityVerificationDappDeepLinkAuthState.cs | 14 +++++++------- .../States/IdentityVerificationOTPAuthState.cs | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs index 44563060835..5a7edbc4457 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs @@ -55,13 +55,13 @@ public override void Exit() if (loginException != null) { spanErrorInfo = loginException switch - { - OperationCanceledException => new SpanErrorInfo("Login process was cancelled by user"), - SignatureExpiredException ex => new SpanErrorInfo("Web3 signature expired during deep-link authentication", ex), - Web3SignatureException ex => new SpanErrorInfo("Web3 signature validation failed during deep-link authentication", ex), - Web3Exception ex => new SpanErrorInfo("Connection error during deep-link authentication flow", ex), - Exception ex => new SpanErrorInfo("Unexpected error during deep-link authentication flow", ex), - }; + { + OperationCanceledException => new SpanErrorInfo("Login process was cancelled by user"), + SignatureExpiredException ex => new SpanErrorInfo("Web3 signature expired during deep-link authentication", ex), + Web3SignatureException ex => new SpanErrorInfo("Web3 signature validation failed during deep-link authentication", ex), + Web3Exception ex => new SpanErrorInfo("Connection error during deep-link authentication flow", ex), + Exception ex => new SpanErrorInfo("Unexpected error during deep-link authentication flow", ex), + }; if (loginException is not OperationCanceledException) ReportHub.LogException(loginException, new ReportData(ReportCategory.AUTHENTICATION)); diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationOTPAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationOTPAuthState.cs index 96f43db1176..0f9360fa542 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationOTPAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationOTPAuthState.cs @@ -66,14 +66,14 @@ public override void Exit() view.Hide(SLIDE); spanErrorInfo = loginException switch - { - OperationCanceledException => new SpanErrorInfo("Login process was cancelled by user"), - SignatureExpiredException ex => new SpanErrorInfo("Web3 signature expired during authentication", ex), - Web3SignatureException ex => new SpanErrorInfo("Web3 signature validation failed", ex), - CodeVerificationException ex => new SpanErrorInfo("Code verification failed during authentication", ex), - InvalidEmailException ex => new SpanErrorInfo("Invalid email provided during authentication", ex), - Exception ex => new SpanErrorInfo("Unexpected error during authentication flow", ex), - }; + { + OperationCanceledException => new SpanErrorInfo("Login process was cancelled by user"), + SignatureExpiredException ex => new SpanErrorInfo("Web3 signature expired during authentication", ex), + Web3SignatureException ex => new SpanErrorInfo("Web3 signature validation failed", ex), + CodeVerificationException ex => new SpanErrorInfo("Code verification failed during authentication", ex), + InvalidEmailException ex => new SpanErrorInfo("Invalid email provided during authentication", ex), + Exception ex => new SpanErrorInfo("Unexpected error during authentication flow", ex), + }; if (loginException is not OperationCanceledException && loginException is not InvalidEmailException) ReportHub.LogException(loginException, new ReportData(ReportCategory.AUTHENTICATION)); From b29abe0489062c1ce89a1aac108cf0dc9076cc0c Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 14:15:10 +0200 Subject: [PATCH 14/54] simplified comments --- .../Global/Dynamic/DynamicWorldContainer.cs | 2 - .../Dynamic/_Scratch/DeeplinkBackendTester.cs | 148 ------------------ .../_Scratch/DeeplinkBackendTester.cs.meta | 2 - .../Implementations/CompositeWeb3Provider.cs | 10 +- .../Dapp/DappDeepLinkAuthenticator.cs | 14 +- .../Dapp/DappWeb3EthereumApi.cs | 7 +- 6 files changed, 10 insertions(+), 173 deletions(-) delete mode 100644 Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs delete mode 100644 Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs.meta diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs index 956aa42d49f..7b5d0d05112 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -475,8 +475,6 @@ await MapRendererContainer // Local scene development scenes are excluded from deeplink runtime handling logic if (!appArgs.HasFlag(AppArgsFlags.LOCAL_SCENE)) { - // Shared with the deep-link login (created in BootstrapContainer): the browser delivers the signin - // deep link here, DeepLinkHandle dispatches it, and DappDeepLinkAuthenticator awaits it. var deepLinkHandleImplementation = new DeepLinkHandle(dynamicWorldParams.StartParcel, chatContainer.ChatTeleporter, ct, communitiesDataService, bootstrapContainer.DeeplinkSigninDispatcher); deepLinkHandleImplementation.StartListenForDeepLinksAsync(ct).Forget(); } diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs deleted file mode 100644 index 65aa7b5c580..00000000000 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs +++ /dev/null @@ -1,148 +0,0 @@ -// THROWAWAY scratch tool — manual verification of deep-link sign-in backend communication. -// NOT production code. No style/architecture rules apply here. Do not commit / do not move into a prod path. -// Sits in a _Scratch subfolder next to BootstrapContainer.cs (which constructs DappDeepLinkAuthenticator with the -// exact same deps), so it lands in Assembly-CSharp and every type resolves. Wrapped in #if UNITY_EDITOR so -// Unity never compiles it into a player build. Do not commit; delete when done. -// -// HOW TO USE (Play Mode required — web requests, OpenUrl): -// 1. Empty scene -> empty GameObject -> attach this component. -// 2. Enter Play Mode (Awake builds the authenticator). -// 3. Right-click the component header (or the gear icon) to get the context menu, then run the steps in order. -// Read the Unity console between steps. -// -// STEPS (context-menu items): -// "1. Login (deeplink)" -> full LoginAsync: POST /requests (mints requestId over HTTP) + opens the browser with -// flow=deeplink. Reaching the browser proves the mint worked. Then it BLOCKS waiting -// for an identityId. -// "2. Dispatch identityId" -> paste an identityId into the Inspector field first, then run this to -// dispatcher.Dispatch(it). Replaces OS deep-link routing; unblocks step 1, which -// then does GET /identities/{id} internally (full E2E). -// "Cancel current" -> cancels the in-flight operation and resets the token. - -#if UNITY_EDITOR -using CommunicationData.URLHelpers; -using Cysharp.Threading.Tasks; -using DCL.Browser; -using DCL.Browser.DecentralandUrls; -using DCL.Multiplayer.Connections.DecentralandUrls; -using DCL.RuntimeDeepLink; -using DCL.Time; -using DCL.Utility; -using DCL.Web3.Accounts.Factory; -using DCL.Web3.Authenticators; -using DCL.Web3.Identities; -using DCL.WebRequests; -using DCL.WebRequests.Analytics; -using DCL.WebRequests.RequestsHub; -using DCL.DebugUtilities.UIBindings; -using System; -using System.Threading; -using UnityEngine; - -public class DeeplinkBackendTester : MonoBehaviour -{ - private const DecentralandEnvironment ENVIRONMENT = DecentralandEnvironment.Zone; - - [Tooltip("Paste the identityId here before running step 2.")] - [SerializeField] private string identityId = ""; - - private DappDeepLinkAuthenticator authenticator; - private DeeplinkSigninDispatcher dispatcher; - private IWebRequestController webRequestController; - private CancellationTokenSource cts; - - private void Awake() - { - cts = new CancellationTokenSource(); - - var urls = DecentralandUrlsSource.CreateForTest(ENVIRONMENT, ILaunchMode.PLAY); - - dispatcher = new DeeplinkSigninDispatcher(); - - webRequestController = new WebRequestController( - new WebRequestsAnalyticsContainer(), - new MemoryWeb3IdentityCache(), - new RequestHub(urls), - new WebRequestBudget(int.MaxValue, new ElementBinding(int.MaxValue)), - new RealmClock()); - - var webBrowser = new UnityAppWebBrowser(urls); - - authenticator = new DappDeepLinkAuthenticator( - webBrowser, - URLAddress.FromString(urls.Url(DecentralandUrl.ApiAuth)), - URLAddress.FromString(urls.Url(DecentralandUrl.AuthSignatureWebApp)), - new Web3AccountFactory(), - webRequestController, - dispatcher, - null); // identityExpirationDuration - - Debug.Log($"[DeeplinkBackendTester] Ready. env={ENVIRONMENT}\n" + - $" auth-api (POST) : {urls.Url(DecentralandUrl.ApiAuth)}\n" + - $" signature (browser): {urls.Url(DecentralandUrl.AuthSignatureWebApp)}"); - } - - private void OnDestroy() - { - cts?.Cancel(); - cts?.Dispose(); - authenticator?.Dispose(); - } - - [ContextMenu("1. Login (deeplink)")] - private void Step1_Login() - { - if (!EnsurePlayMode()) return; - RunLoginAsync().Forget(); - } - - [ContextMenu("2. Dispatch identityId")] - private void Step2_Dispatch() - { - if (!EnsurePlayMode()) return; - - string id = (identityId ?? "").Trim(); - - if (string.IsNullOrEmpty(id)) - { - Debug.LogWarning("[DeeplinkBackendTester] [2] identityId field is empty."); - return; - } - - Debug.Log($"[DeeplinkBackendTester] [2] dispatcher.Dispatch(\"{id}\")"); - dispatcher.Dispatch(id); - } - - [ContextMenu("Cancel current")] - private void CancelCurrent() - { - if (!EnsurePlayMode()) return; - - Debug.Log("[DeeplinkBackendTester] cancelling current operation, resetting token"); - cts.Cancel(); - cts.Dispose(); - cts = new CancellationTokenSource(); - } - - private async UniTaskVoid RunLoginAsync() - { - Debug.Log("[DeeplinkBackendTester] >> [1] LoginAsync START (POST /requests + open browser, then waits for dispatch)"); - - try - { - IWeb3Identity identity = await authenticator.LoginAsync(LoginPayload.ForDappFlow(LoginMethod.GOOGLE), cts.Token); - Debug.Log($"[DeeplinkBackendTester] << [1] LOGIN OK. address={identity.Address} expiration={identity.Expiration:O} source={identity.Source} isExpired={identity.IsExpired}"); - } - catch (OperationCanceledException) { Debug.Log("[DeeplinkBackendTester] << [1] cancelled"); } - catch (Exception e) { Debug.LogError($"[DeeplinkBackendTester] << [1] EXCEPTION: {e.GetType().Name}: {e.Message}\n{e}"); } - } - - private bool EnsurePlayMode() - { - if (authenticator != null) return true; - - Debug.LogWarning("[DeeplinkBackendTester] Not initialized — enter Play Mode first (Awake builds the authenticator)."); - return false; - } -} -#endif diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs.meta b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs.meta deleted file mode 100644 index bdbf888c249..00000000000 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/_Scratch/DeeplinkBackendTester.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 89bb6dbd9477c26439c0b32a02b057c7 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs index d048816e993..07e87728317 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs @@ -17,7 +17,7 @@ namespace DCL.Web3.Authenticators public class CompositeWeb3Provider : ICompositeWeb3Provider { private readonly ThirdWebAuthenticator thirdWebAuth; - private readonly DappWeb3EthereumApi dappAuth; + private readonly DappWeb3EthereumApi dappEthereumApi; private readonly IWeb3Authenticator dappLogin; private readonly IWeb3IdentityCache identityCache; private readonly IAnalyticsController analytics; @@ -33,17 +33,17 @@ public event Action? OTPSendSucceeded public bool IsThirdWebOTP => CurrentProvider == AuthProvider.ThirdWeb; private IWeb3Authenticator currentAuthenticator => CurrentProvider == AuthProvider.ThirdWeb ? thirdWebAuth : dappLogin; - private IEthereumApi currentEthereumApi => CurrentProvider == AuthProvider.ThirdWeb ? thirdWebAuth : dappAuth; + private IEthereumApi currentEthereumApi => CurrentProvider == AuthProvider.ThirdWeb ? thirdWebAuth : dappEthereumApi; public CompositeWeb3Provider( ThirdWebAuthenticator thirdWebAuth, - DappWeb3EthereumApi dappAuth, + DappWeb3EthereumApi dappEthereumApi, DappDeepLinkAuthenticator dappLogin, IWeb3IdentityCache identityCache, IAnalyticsController analytics) { this.thirdWebAuth = thirdWebAuth ?? throw new ArgumentNullException(nameof(thirdWebAuth)); - this.dappAuth = dappAuth ?? throw new ArgumentNullException(nameof(dappAuth)); + this.dappEthereumApi = dappEthereumApi ?? throw new ArgumentNullException(nameof(dappEthereumApi)); this.dappLogin = dappLogin ?? throw new ArgumentNullException(nameof(dappLogin)); this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache)); this.analytics = analytics ?? throw new ArgumentNullException(nameof(analytics)); @@ -52,7 +52,7 @@ public CompositeWeb3Provider( public void Dispose() { thirdWebAuth.Dispose(); - dappAuth.Dispose(); + dappEthereumApi.Dispose(); dappLogin.Dispose(); identityCache.Dispose(); } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index fb7a5d38b09..e51942d892e 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -17,17 +17,9 @@ namespace DCL.Web3.Authenticators { /// - /// Identity-based deep-link sign-in. Mints a requestId with a single POST {authApiUrl}/requests - /// (body: { "method": "dcl_personal_sign", "params": [ephemeralMessage] }), builds the browser URL with - /// flow=deeplink and opens it, then awaits the for the - /// identityId that the browser delivers via an OS-routed deep link, and resolves the identity via - /// GET /identities/{id}. - /// - /// The browser owns the ephemeral keypair in this flow: it generates the keypair, builds the full AuthIdentity - /// and POSTs it to /identities, ignoring any ephemeral params Unity sends. The final identity is therefore - /// resolved entirely from the fetcher; the local ephemeral generated below is NOT used to build it. It only - /// serves as the body of the /requests POST that mints the requestId auth-app's RequestPage needs - /// in its URL path. + /// Production wallet sign-in via browser and OS deep link: initiates a sign-in request on the auth server, + /// opens the browser for the user to sign with their wallet, then awaits the deep link + /// (via ) and resolves the resulting identity from the server. /// public class DappDeepLinkAuthenticator : IWeb3Authenticator { diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs index b08b9fbf8f9..df30f22ef28 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs @@ -135,13 +135,10 @@ public async UniTask SendAsync(EthApiRequest request, Web3Reques /// /// Legacy socket-based wallet sign-in (kept for the Archipelago dev playgrounds via /// ). The production wallet flow is . - /// 1. An authentication request is sent to the server. - /// 2. Open a tab to let the user sign through the browser with his custom installed wallet. - /// 3. Use the signature information to generate the identity. /// /// Login payload containing the authentication method /// - public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) + private async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) { await mutex.WaitAsync(ct); @@ -215,7 +212,7 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio } } - public async UniTask LogoutAsync(CancellationToken ct) => + private async UniTask LogoutAsync(CancellationToken ct) => await DisconnectFromAuthApiAsync(); private async UniTask DisconnectFromAuthApiAsync() From 14ecb96c3eb605a4ab215d90c0167b6ba0872e33 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 14:20:32 +0200 Subject: [PATCH 15/54] removed interface --- .../Global/Dynamic/BootstrapContainer.cs | 4 +- .../DeepLinkHandleImplementation.cs | 4 +- .../DeeplinkSigninDispatcher.cs | 27 +++++++++++++- .../IDeeplinkSigninDispatcher.cs | 37 ------------------- .../IDeeplinkSigninDispatcher.cs.meta | 2 - .../Dapp/DappDeepLinkAuthenticator.cs | 6 +-- 6 files changed, 32 insertions(+), 48 deletions(-) delete mode 100644 Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs delete mode 100644 Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs.meta diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index 76cf640c301..a89a5b44035 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -49,7 +49,7 @@ public class BootstrapContainer : DCLGlobalContainer public IBootstrap? Bootstrap { get; private set; } public IWeb3IdentityCache? IdentityCache { get; private set; } public ICompositeWeb3Provider? CompositeWeb3Provider { get; private set; } - public IDeeplinkSigninDispatcher DeeplinkSigninDispatcher { get; private set; } // Shared single-slot signin deep-link bus. + public DeeplinkSigninDispatcher DeeplinkSigninDispatcher { get; private set; } // Shared single-slot signin deep-link bus. public AnalyticsContainer Analytics { get; private set; } public DebugSettings.DebugSettings DebugSettings { get; private set; } public VolumeBus VolumeBus { get; private set; } @@ -188,7 +188,7 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies( DecentralandEnvironment dclEnvironment, IAppArgs appArgs, IWebRequestController webRequestController, - IDeeplinkSigninDispatcher deeplinkSigninDispatcher) + DeeplinkSigninDispatcher deeplinkSigninDispatcher) { int? identityExpirationDuration = appArgs.TryGetValue(AppArgsFlags.IDENTITY_EXPIRATION_DURATION, out string? v) ? int.Parse(v!) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index 9128a60e5ef..2198137c4f1 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -16,9 +16,9 @@ public class DeepLinkHandle : IDeepLinkHandle private readonly ChatTeleporter chatTeleporter; private readonly CancellationToken token; private readonly CommunityDataService communityDataService; - private readonly IDeeplinkSigninDispatcher deeplinkSigninDispatcher; + private readonly DeeplinkSigninDispatcher deeplinkSigninDispatcher; - public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, CancellationToken token, CommunityDataService communityDataService, IDeeplinkSigninDispatcher deeplinkSigninDispatcher) + public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, CancellationToken token, CommunityDataService communityDataService, DeeplinkSigninDispatcher deeplinkSigninDispatcher) { this.startParcel = startParcel; this.chatTeleporter = chatTeleporter; diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs index 90252365444..df7494ae0ce 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs @@ -2,13 +2,28 @@ namespace DCL.RuntimeDeepLink { - /// - public class DeeplinkSigninDispatcher : IDeeplinkSigninDispatcher + /// + /// Single-event pub/sub for signin deep links. Bridges the producer (DeepLinkHandle, which parses + /// the runtime-arriving signin deep link) and the consumer (the deep-link login, which awaits it). Only a + /// live subscriber receives a signin; there is no buffering, so a signin dispatched with no subscriber is dropped. + /// + public class DeeplinkSigninDispatcher { private Subscription? current; + /// + /// Whether a live subscriber is currently listening for signin deep links. + /// public bool HasSubscriber => current != null; + /// + /// Subscribes to incoming signin deep links. Dispose the returned handle to stop listening. + /// + /// Invoked with the identityId carried by the deep link. + /// + /// Forward-compatible correlation key. Unused in Stage 1; in Stage 2 the dispatcher drops any dispatch + /// whose sourceRequestId does not match. + /// public IDisposable Subscribe(Action onSigninReceived, string? expectedRequestId = null) { var subscription = new Subscription(this, onSigninReceived); @@ -16,6 +31,14 @@ public IDisposable Subscribe(Action onSigninReceived, string? expectedRe return subscription; } + /// + /// Delivers a signin deep link to the active subscriber, if any. Dispatched with no subscriber, the signin + /// is dropped; callers gate on before dispatching. + /// + /// The opaque identity id carried by the deep link. + /// + /// Forward-compatible correlation key carried by the deep link. Unused in Stage 1. + /// public void Dispatch(string identityId, string? sourceRequestId = null) { // Stage 1: the sourceRequestId / expectedRequestId correlation is intentionally not yet applied. diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs b/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs deleted file mode 100644 index 665567dc689..00000000000 --- a/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; - -namespace DCL.RuntimeDeepLink -{ - /// - /// Single-event pub/sub for signin deep links. Bridges the producer (DeepLinkHandle, which parses - /// the runtime-arriving signin deep link) and the consumer (the deep-link login, which awaits it). Only a - /// live subscriber receives a signin; there is no buffering, so a signin dispatched with no subscriber is dropped. - /// - public interface IDeeplinkSigninDispatcher - { - /// - /// Whether a live subscriber is currently listening for signin deep links. - /// - bool HasSubscriber { get; } - - /// - /// Subscribes to incoming signin deep links. Dispose the returned handle to stop listening. - /// - /// Invoked with the identityId carried by the deep link. - /// - /// Forward-compatible correlation key. Unused in Stage 1; in Stage 2 the dispatcher drops any dispatch - /// whose sourceRequestId does not match. - /// - IDisposable Subscribe(Action onSigninReceived, string? expectedRequestId = null); - - /// - /// Delivers a signin deep link to the active subscriber, if any. Dispatched with no subscriber, the signin - /// is dropped; callers gate on before dispatching. - /// - /// The opaque identity id carried by the deep link. - /// - /// Forward-compatible correlation key carried by the deep link. Unused in Stage 1. - /// - void Dispatch(string identityId, string? sourceRequestId = null); - } -} diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs.meta b/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs.meta deleted file mode 100644 index c1e71a01f0e..00000000000 --- a/Explorer/Assets/DCL/RuntimeDeepLink/IDeeplinkSigninDispatcher.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a9d69ed0b48bbb349ab013bb7f5ae765 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index e51942d892e..e879e6f1869 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -19,7 +19,7 @@ namespace DCL.Web3.Authenticators /// /// Production wallet sign-in via browser and OS deep link: initiates a sign-in request on the auth server, /// opens the browser for the user to sign with their wallet, then awaits the deep link - /// (via ) and resolves the resulting identity from the server. + /// (via ) and resolves the resulting identity from the server. /// public class DappDeepLinkAuthenticator : IWeb3Authenticator { @@ -35,7 +35,7 @@ public class DappDeepLinkAuthenticator : IWeb3Authenticator private readonly URLAddress signatureWebAppUrl; private readonly IWeb3AccountFactory web3AccountFactory; private readonly IWebRequestController webRequestController; - private readonly IDeeplinkSigninDispatcher deeplinkSigninDispatcher; + private readonly DeeplinkSigninDispatcher deeplinkSigninDispatcher; private readonly int? identityExpirationDuration; private readonly URLBuilder urlBuilder = new (); @@ -45,7 +45,7 @@ public DappDeepLinkAuthenticator( URLAddress signatureWebAppUrl, IWeb3AccountFactory web3AccountFactory, IWebRequestController webRequestController, - IDeeplinkSigninDispatcher deeplinkSigninDispatcher, + DeeplinkSigninDispatcher deeplinkSigninDispatcher, int? identityExpirationDuration = null) { this.webBrowser = webBrowser; From cfef516c0325f0fa4eb71c89abf8d827f88fcdfa Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 15:03:26 +0200 Subject: [PATCH 16/54] clean up logs and comments --- .../ReportsHandlingSettingsDevelopment.asset | 4 ---- .../ReportsHandlingSettingsProduction.asset | 12 ---------- .../Dapp/DappDeepLinkAuthenticator.cs | 24 ++++++------------- 3 files changed, 7 insertions(+), 33 deletions(-) diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset index 7f9c1cb4c7f..c69c1771a1c 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset @@ -708,10 +708,6 @@ MonoBehaviour: Severity: 2 - Category: MULTIPLAYER Severity: 2 - - Category: RUNTIME_DEEPLINKS - Severity: 3 - - Category: RUNTIME_DEEPLINKS - Severity: 2 sentryMatrix: entries: [] debounceEnabled: 1 diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset index baef4f8fee8..57f1e67ae01 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset @@ -448,24 +448,12 @@ MonoBehaviour: Severity: 0 - Category: MULTIPLAYER Severity: 2 - - Category: UNSPECIFIED - Severity: 3 - - Category: UNSPECIFIED - Severity: 2 - - Category: UNSPECIFIED - Severity: 0 - Category: AUTHENTICATION Severity: 3 - Category: AUTHENTICATION Severity: 2 - Category: AUTHENTICATION Severity: 0 - - Category: RUNTIME_DEEPLINKS - Severity: 3 - - Category: RUNTIME_DEEPLINKS - Severity: 2 - - Category: RUNTIME_DEEPLINKS - Severity: 0 sentryMatrix: entries: - Category: AUDIO_SOURCES diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index e879e6f1869..d26f3c7ee5b 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -7,6 +7,7 @@ using DCL.Web3.Chains; using DCL.Web3.Identities; using DCL.WebRequests; +using JetBrains.Annotations; using Nethereum.Signer; using Newtonsoft.Json; using System; @@ -25,9 +26,6 @@ public class DappDeepLinkAuthenticator : IWeb3Authenticator { // Fallback session length when no explicit override is provided (days). private const double IDENTITY_EXPIRATION_PERIOD = 30; - - // The deep-link flow waits for the user to sign in their browser and for the OS to route the resulting - // deep link back to this process; this can take much longer than a socket round-trip. private const int DEEPLINK_TIMEOUT_SECONDS = 300; private readonly IWebBrowser webBrowser; @@ -61,8 +59,7 @@ public void Dispose() { } public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) { - // The local ephemeral is not used to build the final identity (the browser owns the real keypair; we - // resolve via the fetcher), but it is the well-formed message the server signs into the minted request. + // The local ephemeral is not used to build the final identity, but it is the well-formed message the server signs into the minted request. var ephemeralAccount = web3AccountFactory.CreateRandomAccount(); DateTime sessionExpiration = identityExpirationDuration != null @@ -94,8 +91,8 @@ public UniTask LogoutAsync(CancellationToken ct) => UniTask.CompletedTask; /// - /// Mints a sign-in requestId via POST {authApiUrl}/requests. The browser later recovers the - /// request by that id to drive the wallet signature. + /// Mints a sign-in requestId via POST {authApiUrl}/requests. + /// The browser later recovers the request by that id to drive the wallet signature. /// private async UniTask CreateSigninRequestAsync(string ephemeralMessage, CancellationToken ct) { @@ -117,9 +114,7 @@ private async UniTask CreateSigninRequestAsync(string } /// - /// Awaits the first identityId the dispatcher delivers, applying a timeout and honoring external - /// cancellation. Disposing the subscription on completion clears the dispatcher buffer so a deep link consumed - /// (or cancelled) by one attempt does not bleed into the next. + /// Awaits the first identityId the dispatcher delivers /// private async UniTask WaitForSigninAsync(CancellationToken ct) { @@ -127,15 +122,9 @@ private async UniTask WaitForSigninAsync(CancellationToken ct) using IDisposable subscription = deeplinkSigninDispatcher.Subscribe(identityId => completionSource.TrySetResult(identityId)); - // Realtime, not the default DeltaTime: the wait spans the user switching to the browser and back, during - // which the app is backgrounded and game time stalls. Only wall-clock measures the deadline correctly. return await completionSource.Task.Timeout(TimeSpan.FromSeconds(DEEPLINK_TIMEOUT_SECONDS), DelayType.Realtime).AttachExternalCancellation(ct); } - /// - /// Fetches a stored identity by its opaque id via GET {authApiUrl}/identities/{id} and reconstructs a - /// fully-formed . - /// private async UniTask FetchIdentityByIdAsync(string identityId, CancellationToken ct) { urlBuilder.Clear(); @@ -160,10 +149,11 @@ private async UniTask FetchIdentityByIdAsync(string identi return new DecentralandIdentity(new Web3Address(address), ephemeralAccount, expiration, authChain, IWeb3Identity.Web3IdentitySource.Deeplink); } - private string CreateEphemeralMessage(IWeb3Account ephemeralAccount, DateTime expiration) => + private static string CreateEphemeralMessage(IWeb3Account ephemeralAccount, DateTime expiration) => $"Decentraland Login\nEphemeral address: {ephemeralAccount.Address.OriginalFormat}\nExpiration: {expiration:yyyy-MM-ddTHH:mm:ss.fffZ}"; [Serializable] + [PublicAPI] private struct SigninRequestDto { public string method; From 8c84cdc2617e6db52f7605a613c1f2a8d24a68c6 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 15:14:57 +0200 Subject: [PATCH 17/54] more cleanup --- .../DCL/RuntimeDeepLink/DeepLinkHandle.cs | 1 + .../DeepLinkHandleImplementation.cs | 13 ++++-------- .../DeeplinkSigninDispatcher.cs | 21 +++++-------------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs index 3af698eebcd..98cef348674 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs @@ -14,6 +14,7 @@ class Null : IDeepLinkHandle private Null() { } + public bool HandleDeepLink(DeepLink deeplink) => true; } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index 2198137c4f1..566f22fa6fd 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -27,23 +27,18 @@ public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, Ca this.deeplinkSigninDispatcher = deeplinkSigninDispatcher; } - public string Name => "Real Implementation"; - public bool HandleDeepLink(DeepLink deeplink) { - // Signin takes precedence over realm/position/community routing and returns before any teleport or - // community notification is triggered. + // Signin takes precedence over realm/position/community routing and returns before any teleport or community notification is triggered. string? signin = deeplink.ValueOf(AppArgsFlags.SIGNIN); if (!string.IsNullOrEmpty(signin)) { - // Only consume the signin when a login flow is actively awaiting it; otherwise leave the bridge - // file in place so the instance that is logging in can pick it up. if (!deeplinkSigninDispatcher.HasSubscriber) return false; deeplinkSigninDispatcher.Dispatch(signin); - ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"{Name} dispatched signin deeplink: {deeplink}"); + ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"dispatched signin deeplink: {deeplink}"); return true; } @@ -81,9 +76,9 @@ public bool HandleDeepLink(DeepLink deeplink) } if (handled) - ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"{Name} successfully handled deeplink: {deeplink}"); + ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"successfully handled deeplink: {deeplink}"); else - ReportHub.LogWarning(ReportCategory.RUNTIME_DEEPLINKS, $"{Name} found no actionable content in deeplink: {deeplink}"); + ReportHub.LogWarning(ReportCategory.RUNTIME_DEEPLINKS, $"found no actionable content in deeplink: {deeplink}"); // Non-signin deep links are always consumed: nothing awaits them, so leaving the file would re-loop. return true; diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs index df7494ae0ce..84c3a38ab51 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs @@ -3,9 +3,7 @@ namespace DCL.RuntimeDeepLink { /// - /// Single-event pub/sub for signin deep links. Bridges the producer (DeepLinkHandle, which parses - /// the runtime-arriving signin deep link) and the consumer (the deep-link login, which awaits it). Only a - /// live subscriber receives a signin; there is no buffering, so a signin dispatched with no subscriber is dropped. + /// Single-event pub/sub for signin deep links. /// public class DeeplinkSigninDispatcher { @@ -17,14 +15,10 @@ public class DeeplinkSigninDispatcher public bool HasSubscriber => current != null; /// - /// Subscribes to incoming signin deep links. Dispose the returned handle to stop listening. + /// Subscribes to incoming signin deep links. /// /// Invoked with the identityId carried by the deep link. - /// - /// Forward-compatible correlation key. Unused in Stage 1; in Stage 2 the dispatcher drops any dispatch - /// whose sourceRequestId does not match. - /// - public IDisposable Subscribe(Action onSigninReceived, string? expectedRequestId = null) + public IDisposable Subscribe(Action onSigninReceived) { var subscription = new Subscription(this, onSigninReceived); current = subscription; @@ -32,16 +26,11 @@ public IDisposable Subscribe(Action onSigninReceived, string? expectedRe } /// - /// Delivers a signin deep link to the active subscriber, if any. Dispatched with no subscriber, the signin - /// is dropped; callers gate on before dispatching. + /// Delivers a signin deep link to the active subscriber, if any. /// /// The opaque identity id carried by the deep link. - /// - /// Forward-compatible correlation key carried by the deep link. Unused in Stage 1. - /// - public void Dispatch(string identityId, string? sourceRequestId = null) + public void Dispatch(string identityId) { - // Stage 1: the sourceRequestId / expectedRequestId correlation is intentionally not yet applied. current?.Handler(identityId); } From ecc5b31c156ab15218fd8535dc73b1c7658f5b6f Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 18:41:15 +0200 Subject: [PATCH 18/54] add sanity-check for received address --- .../Dapp/DappDeepLinkAuthenticator.cs | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index d26f3c7ee5b..9aa3b82e2cc 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -137,16 +137,38 @@ private async UniTask FetchIdentityByIdAsync(string identi IdentityAuthResponseDto json = await webRequestController.GetAsync(commonArguments, ct, ReportCategory.AUTHENTICATION) .CreateFromNewtonsoftJsonAsync(); + string? signerAddress = null; + string? ephemeralPayload = null; + + foreach (AuthLink authLink in json.identity.authChain) + { + if (authLink.type == AuthLinkType.SIGNER) + signerAddress = authLink.payload; + else if (authLink.type is AuthLinkType.ECDSA_EPHEMERAL or AuthLinkType.ECDSA_EIP_1654_EPHEMERAL) + ephemeralPayload = authLink.payload; + } + + if (signerAddress is not { Length: > 0 }) + throw new Web3Exception($"Sign-in identity {identityId} has no SIGNER link in its auth chain"); + + if (ephemeralPayload is not { Length: > 0 }) + throw new Web3Exception($"Sign-in identity {identityId} has no ephemeral link in its auth chain"); + + IWeb3Account ephemeralAccount = web3AccountFactory.CreateAccount(new EthECKey(json.identity.ephemeralIdentity.privateKey)); + + // Every signed request is made with this ephemeral key: if it does not match the address the wallet + // signed into the ephemeral link, servers reject every signature. Fail fast at login instead. + if (!ephemeralPayload.Contains(ephemeralAccount.Address, StringComparison.OrdinalIgnoreCase)) + throw new Web3Exception($"Sign-in identity {identityId} is inconsistent: the ephemeral private key does not match the auth chain ephemeral address {ephemeralAccount.Address}"); + var authChain = AuthChain.Create(); foreach (AuthLink authLink in json.identity.authChain) authChain.Set(authLink); - string address = authChain.Get(AuthLinkType.SIGNER).payload; - IWeb3Account ephemeralAccount = web3AccountFactory.CreateAccount(new EthECKey(json.identity.ephemeralIdentity.privateKey)); DateTime expiration = DateTime.Parse(json.identity.expiration, null, DateTimeStyles.RoundtripKind); - return new DecentralandIdentity(new Web3Address(address), ephemeralAccount, expiration, authChain, IWeb3Identity.Web3IdentitySource.Deeplink); + return new DecentralandIdentity(new Web3Address(signerAddress), ephemeralAccount, expiration, authChain, IWeb3Identity.Web3IdentitySource.Deeplink); } private static string CreateEphemeralMessage(IWeb3Account ephemeralAccount, DateTime expiration) => From 91a0d4e3f09312e78d89f727dc86d6270f411722 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 19:54:27 +0200 Subject: [PATCH 19/54] addressed several reviewer requests --- ...entityVerificationDappDeepLinkAuthState.cs | 8 ++- .../Global/AppArgs/AppArgsFlags.cs | 3 +- .../Global/Dynamic/BootstrapContainer.cs | 11 ++-- .../Global/Dynamic/DynamicWorldContainer.cs | 2 +- .../DCL/RuntimeDeepLink/DeepLinkHandle.cs | 16 ++--- .../DeepLinkHandleImplementation.cs | 28 +++------ .../DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 15 ++++- .../DeeplinkSigninDispatcher.cs | 63 ------------------- .../DeeplinkSigninDispatcher.cs.meta | 2 - .../Dapp/DappDeepLinkAuthenticator.cs | 41 +++++++----- .../Dapp/DappWeb3EthereumApi.cs | 4 +- 11 files changed, 72 insertions(+), 121 deletions(-) delete mode 100644 Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs delete mode 100644 Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs.meta diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs index 5a7edbc4457..46e3b905181 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs @@ -56,7 +56,8 @@ public override void Exit() { spanErrorInfo = loginException switch { - OperationCanceledException => new SpanErrorInfo("Login process was cancelled by user"), + OperationCanceledException ex => new SpanErrorInfo("Login process was cancelled by user", ex), + TimeoutException ex => new SpanErrorInfo("Deep-link sign-in timed out waiting for the browser to deliver the signin deep link", ex), SignatureExpiredException ex => new SpanErrorInfo("Web3 signature expired during deep-link authentication", ex), Web3SignatureException ex => new SpanErrorInfo("Web3 signature validation failed during deep-link authentication", ex), Web3Exception ex => new SpanErrorInfo("Connection error during deep-link authentication flow", ex), @@ -84,6 +85,11 @@ private async UniTaskVoid AuthenticateAsync(LoginMethod method, CancellationToke loginException = e; machine.Enter(SLIDE); } + catch (TimeoutException e) + { + loginException = e; + machine.Enter(SLIDE); + } catch (SignatureExpiredException e) { loginException = e; diff --git a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs index 737140e96ca..cfef6568a04 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs @@ -31,7 +31,8 @@ public static class AppArgsFlags /// public const string COMMUNITY = "community"; - public const string SIGNIN = "signin"; /// The opaque identity id delivered by the auth website's signin deep link (decentraland://?signin={identityId}). + // The opaque identity id delivered by the auth website's signin deep link (decentraland://?signin={identityId}). + public const string SIGNIN = "signin"; public const string FORCED_EMOTES = "self-force-emotes"; public const string SELF_PREVIEW_EMOTES = "self-preview-emotes"; diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index a89a5b44035..95983c4289e 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -11,9 +11,9 @@ using DCL.PerformanceAndDiagnostics.Analytics; using DCL.PluginSystem; using DCL.PluginSystem.Global; -using DCL.RuntimeDeepLink; using DCL.SceneLoadingScreens.SplashScreen; using DCL.Time; +using DCL.Utilities; using DCL.Utility; using DCL.Web3.Abstract; using DCL.Web3.Accounts.Factory; @@ -49,7 +49,7 @@ public class BootstrapContainer : DCLGlobalContainer public IBootstrap? Bootstrap { get; private set; } public IWeb3IdentityCache? IdentityCache { get; private set; } public ICompositeWeb3Provider? CompositeWeb3Provider { get; private set; } - public DeeplinkSigninDispatcher DeeplinkSigninDispatcher { get; private set; } // Shared single-slot signin deep-link bus. + public ReactiveProperty DeeplinkSigninIdentityId { get; } = new (null); public AnalyticsContainer Analytics { get; private set; } public DebugSettings.DebugSettings DebugSettings { get; private set; } public VolumeBus VolumeBus { get; private set; } @@ -131,8 +131,7 @@ await bootstrapContainer.InitializeContainerAsync deeplinkSigninIdentityId) { int? identityExpirationDuration = appArgs.TryGetValue(AppArgsFlags.IDENTITY_EXPIRATION_DURATION, out string? v) ? int.Parse(v!) @@ -211,7 +210,7 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies( URLAddress.FromString(decentralandUrlsSource.Url(DecentralandUrl.AuthSignatureWebApp)), web3AccountFactory, webRequestController, - deeplinkSigninDispatcher, + deeplinkSigninIdentityId, identityExpirationDuration ); diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs index 7b5d0d05112..dd3a155d69e 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -475,7 +475,7 @@ await MapRendererContainer // Local scene development scenes are excluded from deeplink runtime handling logic if (!appArgs.HasFlag(AppArgsFlags.LOCAL_SCENE)) { - var deepLinkHandleImplementation = new DeepLinkHandle(dynamicWorldParams.StartParcel, chatContainer.ChatTeleporter, ct, communitiesDataService, bootstrapContainer.DeeplinkSigninDispatcher); + var deepLinkHandleImplementation = new DeepLinkHandle(dynamicWorldParams.StartParcel, chatContainer.ChatTeleporter, ct, communitiesDataService, bootstrapContainer.DeeplinkSigninIdentityId); deepLinkHandleImplementation.StartListenForDeepLinksAsync(ct).Forget(); } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs index 98cef348674..5bff79a533a 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs @@ -1,12 +1,14 @@ namespace DCL.RuntimeDeepLink { + public enum DeepLinkHandleResult + { + Consumed, + NoMatches, + } + public interface IDeepLinkHandle { - /// - /// Returns true when the deep link was consumed and its bridge file may be deleted; - /// false to leave the bridge file in place for a later attempt. - /// - public bool HandleDeepLink(DeepLink deeplink); + DeepLinkHandleResult HandleDeepLink(DeepLink deeplink); class Null : IDeepLinkHandle { @@ -15,8 +17,8 @@ class Null : IDeepLinkHandle private Null() { } - public bool HandleDeepLink(DeepLink deeplink) => - true; + public DeepLinkHandleResult HandleDeepLink(DeepLink deeplink) => + DeepLinkHandleResult.Consumed; } } } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index 566f22fa6fd..563c07e5932 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -2,8 +2,8 @@ using Cysharp.Threading.Tasks; using DCL.Chat.Commands; using DCL.Communities; -using DCL.Diagnostics; using DCL.RealmNavigation; +using DCL.Utilities; using Global.AppArgs; using System.Threading; using UnityEngine; @@ -16,30 +16,26 @@ public class DeepLinkHandle : IDeepLinkHandle private readonly ChatTeleporter chatTeleporter; private readonly CancellationToken token; private readonly CommunityDataService communityDataService; - private readonly DeeplinkSigninDispatcher deeplinkSigninDispatcher; + private readonly ReactiveProperty deeplinkSigninIdentityId; - public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, CancellationToken token, CommunityDataService communityDataService, DeeplinkSigninDispatcher deeplinkSigninDispatcher) + public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, CancellationToken token, CommunityDataService communityDataService, ReactiveProperty deeplinkSigninIdentityId) { this.startParcel = startParcel; this.chatTeleporter = chatTeleporter; this.token = token; this.communityDataService = communityDataService; - this.deeplinkSigninDispatcher = deeplinkSigninDispatcher; + this.deeplinkSigninIdentityId = deeplinkSigninIdentityId; } - public bool HandleDeepLink(DeepLink deeplink) + public DeepLinkHandleResult HandleDeepLink(DeepLink deeplink) { - // Signin takes precedence over realm/position/community routing and returns before any teleport or community notification is triggered. string? signin = deeplink.ValueOf(AppArgsFlags.SIGNIN); if (!string.IsNullOrEmpty(signin)) { - if (!deeplinkSigninDispatcher.HasSubscriber) - return false; - - deeplinkSigninDispatcher.Dispatch(signin); - ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"dispatched signin deeplink: {deeplink}"); - return true; + // The property retains the id, so a login flow that subscribes later still receives it. + deeplinkSigninIdentityId.Value = signin; + return DeepLinkHandleResult.Consumed; } Vector2Int? position = PositionFrom(deeplink); @@ -75,13 +71,7 @@ public bool HandleDeepLink(DeepLink deeplink) handled = true; } - if (handled) - ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"successfully handled deeplink: {deeplink}"); - else - ReportHub.LogWarning(ReportCategory.RUNTIME_DEEPLINKS, $"found no actionable content in deeplink: {deeplink}"); - - // Non-signin deep links are always consumed: nothing awaits them, so leaving the file would re-loop. - return true; + return handled ? DeepLinkHandleResult.Consumed : DeepLinkHandleResult.NoMatches; } private static URLDomain? RealmFrom(DeepLink deepLink) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index 1537fdcc219..22aec8f204d 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -58,9 +58,18 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl continue; } - // A false result means no login flow is awaiting the signin yet: keep the file so it can be picked up later. - if (handle.HandleDeepLink(deepLinkCreateResult.Value)) - TryDeleteBridgeFile(); + switch (handle.HandleDeepLink(deepLinkCreateResult.Value)) + { + case DeepLinkHandleResult.Consumed: + ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"successfully handled deeplink: {deepLinkCreateResult.Value}"); + break; + case DeepLinkHandleResult.NoMatches: + ReportHub.LogWarning(ReportCategory.RUNTIME_DEEPLINKS, $"found no actionable content in deeplink: {deepLinkCreateResult.Value}"); + break; + } + + // Unmatched links are dropped as well: keeping the file would re-read it on every check-in. + TryDeleteBridgeFile(); } } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs deleted file mode 100644 index 84c3a38ab51..00000000000 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; - -namespace DCL.RuntimeDeepLink -{ - /// - /// Single-event pub/sub for signin deep links. - /// - public class DeeplinkSigninDispatcher - { - private Subscription? current; - - /// - /// Whether a live subscriber is currently listening for signin deep links. - /// - public bool HasSubscriber => current != null; - - /// - /// Subscribes to incoming signin deep links. - /// - /// Invoked with the identityId carried by the deep link. - public IDisposable Subscribe(Action onSigninReceived) - { - var subscription = new Subscription(this, onSigninReceived); - current = subscription; - return subscription; - } - - /// - /// Delivers a signin deep link to the active subscriber, if any. - /// - /// The opaque identity id carried by the deep link. - public void Dispatch(string identityId) - { - current?.Handler(identityId); - } - - private void Remove(Subscription subscription) - { - if (current != subscription) - return; - - current = null; - } - - private sealed class Subscription : IDisposable - { - public readonly Action Handler; - private DeeplinkSigninDispatcher? owner; - - public Subscription(DeeplinkSigninDispatcher owner, Action handler) - { - this.owner = owner; - Handler = handler; - } - - public void Dispose() - { - owner?.Remove(this); - owner = null; - } - } - } -} diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs.meta b/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs.meta deleted file mode 100644 index 7fbd3e47dd4..00000000000 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeeplinkSigninDispatcher.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a6c57a40992ea0a4ba7b6eb9185cde36 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index 9aa3b82e2cc..f098716318f 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -2,7 +2,7 @@ using Cysharp.Threading.Tasks; using DCL.Browser; using DCL.Diagnostics; -using DCL.RuntimeDeepLink; +using DCL.Utilities; using DCL.Web3.Abstract; using DCL.Web3.Chains; using DCL.Web3.Identities; @@ -19,13 +19,12 @@ namespace DCL.Web3.Authenticators { /// /// Production wallet sign-in via browser and OS deep link: initiates a sign-in request on the auth server, - /// opens the browser for the user to sign with their wallet, then awaits the deep link - /// (via ) and resolves the resulting identity from the server. + /// opens the browser for the user to sign with their wallet, then awaits the deep link identity id + /// and resolves the resulting identity from the server. /// public class DappDeepLinkAuthenticator : IWeb3Authenticator { - // Fallback session length when no explicit override is provided (days). - private const double IDENTITY_EXPIRATION_PERIOD = 30; + private const double IDENTITY_EXPIRATION_PERIOD_FALLBACK_IN_DAYS = 30; private const int DEEPLINK_TIMEOUT_SECONDS = 300; private readonly IWebBrowser webBrowser; @@ -33,7 +32,7 @@ public class DappDeepLinkAuthenticator : IWeb3Authenticator private readonly URLAddress signatureWebAppUrl; private readonly IWeb3AccountFactory web3AccountFactory; private readonly IWebRequestController webRequestController; - private readonly DeeplinkSigninDispatcher deeplinkSigninDispatcher; + private readonly ReactiveProperty deeplinkSigninIdentityId; private readonly int? identityExpirationDuration; private readonly URLBuilder urlBuilder = new (); @@ -43,7 +42,7 @@ public DappDeepLinkAuthenticator( URLAddress signatureWebAppUrl, IWeb3AccountFactory web3AccountFactory, IWebRequestController webRequestController, - DeeplinkSigninDispatcher deeplinkSigninDispatcher, + ReactiveProperty deeplinkSigninIdentityId, int? identityExpirationDuration = null) { this.webBrowser = webBrowser; @@ -51,7 +50,7 @@ public DappDeepLinkAuthenticator( this.signatureWebAppUrl = signatureWebAppUrl; this.web3AccountFactory = web3AccountFactory; this.webRequestController = webRequestController; - this.deeplinkSigninDispatcher = deeplinkSigninDispatcher; + this.deeplinkSigninIdentityId = deeplinkSigninIdentityId; this.identityExpirationDuration = identityExpirationDuration; } @@ -59,12 +58,12 @@ public void Dispose() { } public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) { - // The local ephemeral is not used to build the final identity, but it is the well-formed message the server signs into the minted request. + // The ephemeral address is embedded in the signed message so the server can mint a well-formed request from it. var ephemeralAccount = web3AccountFactory.CreateRandomAccount(); DateTime sessionExpiration = identityExpirationDuration != null ? DateTime.UtcNow.AddSeconds(identityExpirationDuration.Value) - : DateTime.UtcNow.AddDays(IDENTITY_EXPIRATION_PERIOD); + : DateTime.UtcNow.AddDays(IDENTITY_EXPIRATION_PERIOD_FALLBACK_IN_DAYS); string ephemeralMessage = CreateEphemeralMessage(ephemeralAccount, sessionExpiration); @@ -80,8 +79,8 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio webBrowser.OpenUrl(url); - // The browser builds and stores the AuthIdentity, then opens decentraland://?signin={identityId}; - // the OS routes it to DeepLinkHandle, which dispatches it here. + // The browser builds and stores the AuthIdentity, then opens decentraland://?signin={identityId}, + // which is delivered here through the deep link pipeline. string identityId = await WaitForSigninAsync(ct); return await FetchIdentityByIdAsync(identityId, ct); @@ -114,15 +113,25 @@ private async UniTask CreateSigninRequestAsync(string } /// - /// Awaits the first identityId the dispatcher delivers + /// Awaits the first non-empty identityId, starting from the currently stored one, and consumes it. /// private async UniTask WaitForSigninAsync(CancellationToken ct) { var completionSource = new UniTaskCompletionSource(); - using IDisposable subscription = deeplinkSigninDispatcher.Subscribe(identityId => completionSource.TrySetResult(identityId)); + using var subscription = deeplinkSigninIdentityId.UseCurrentValueAndSubscribeToUpdate(completionSource, + static (identityId, completion) => + { + if (!string.IsNullOrEmpty(identityId)) + completion.TrySetResult(identityId); + }, ct); - return await completionSource.Task.Timeout(TimeSpan.FromSeconds(DEEPLINK_TIMEOUT_SECONDS), DelayType.Realtime).AttachExternalCancellation(ct); + string identityId = await completionSource.Task.Timeout(TimeSpan.FromSeconds(DEEPLINK_TIMEOUT_SECONDS), DelayType.Realtime).AttachExternalCancellation(ct); + + // Consume the id so a future login does not resolve against this same signin. + deeplinkSigninIdentityId.Value = null; + + return identityId; } private async UniTask FetchIdentityByIdAsync(string identityId, CancellationToken ct) @@ -175,7 +184,7 @@ private static string CreateEphemeralMessage(IWeb3Account ephemeralAccount, Date $"Decentraland Login\nEphemeral address: {ephemeralAccount.Address.OriginalFormat}\nExpiration: {expiration:yyyy-MM-ddTHH:mm:ss.fffZ}"; [Serializable] - [PublicAPI] + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] private struct SigninRequestDto { public string method; diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs index df30f22ef28..c9d3e70273d 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs @@ -22,7 +22,7 @@ namespace DCL.Web3.Authenticators { public partial class DappWeb3EthereumApi : IEthereumApi { - private const double IDENTITY_EXPIRATION_PERIOD = 30; + private const double IDENTITY_EXPIRATION_PERIOD_FALLBACK_IN_DAYS = 30; private const int TIMEOUT_SECONDS = 30; private const int RPC_BUFFER_SIZE = 50000; @@ -157,7 +157,7 @@ private async UniTask LoginAsync(LoginPayload payload, Cancellati // 1 week expiration day, just like unity-renderer DateTime sessionExpiration = identityExpirationDuration != null ? DateTime.UtcNow.AddSeconds(identityExpirationDuration.Value) - : DateTime.UtcNow.AddDays(IDENTITY_EXPIRATION_PERIOD); + : DateTime.UtcNow.AddDays(IDENTITY_EXPIRATION_PERIOD_FALLBACK_IN_DAYS); string ephemeralMessage = CreateEphemeralMessage(ephemeralAccount, sessionExpiration); From 64fa111356b498891c923e0f1333252d85b7a2a3 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 20:00:39 +0200 Subject: [PATCH 20/54] removed IWebBrowser and renamed method --- .../BannedNotificationHandler.cs | 6 +- .../BlockedScreenController.cs | 6 +- .../UI/MinimumSpecsScreenController.cs | 6 +- .../ApplicationVersionGuard.cs | 6 +- .../AuthenticationScreenController.cs | 6 +- .../States/LobbyForNewAccountAuthState.cs | 6 +- .../States/LoginSelectionAuthState.cs | 6 +- .../AvatarSection/AvatarController.cs | 6 +- .../AvatarSection/Outfits/OutfitsPresenter.cs | 8 +- .../Assets/DCL/Backpack/BackpackController.cs | 2 +- .../DCL/Backpack/BackpackGridController.cs | 6 +- .../BackpackEmoteGridController.cs | 6 +- .../GiftTransfer/GiftTransferController.cs | 6 +- .../CommunitiesBrowserController.cs | 4 +- .../CommunityCardController.cs | 4 +- .../Events/EventListController.cs | 8 +- .../Members/MembersListController.cs | 4 +- .../Places/PlacesSectionController.cs | 2 +- .../CommunityCreationEditionController.cs | 12 +- .../Donations/UI/DonationsPanelController.cs | 8 +- .../DCL/Events/EventCardActionsController.cs | 10 +- .../DCL/Events/EventDetailPanelController.cs | 4 +- .../Assets/DCL/Events/EventsController.cs | 6 +- .../DCL/EventsApi/GoogleUserCalendar.cs | 6 +- .../ExternalUrlPromptController.cs | 10 +- .../UI/FriendPanel/FriendsPanelController.cs | 2 +- .../Requests/RequestsSectionController.cs | 4 +- .../CameraReelGalleryController.cs | 4 +- .../Systems/InWorldCameraPlugin.cs | 4 +- .../PhotoDetail/EquippedWearableController.cs | 6 +- .../PhotoDetail/PhotoDetailController.cs | 4 +- .../PhotoDetail/PhotoDetailInfoController.cs | 2 +- .../PhotoDetail/PhotoDetailPoolManager.cs | 4 +- .../ReelActions/ReelCommonActions.cs | 4 +- .../Global/Dynamic/BootstrapContainer.cs | 4 +- .../Global/Dynamic/DynamicWorldContainer.cs | 2 +- .../Global/Dynamic/MainSceneLoader.cs | 4 +- .../MVCFacade/MVCManagerMenusAccessFacade.cs | 4 +- .../MarketplaceCreditsMenuController.cs | 8 +- ...etplaceCreditsProgramEndedSubController.cs | 10 +- .../MarketplaceCreditsWelcomeSubController.cs | 6 +- .../DCL/Navmap/EventInfoPanelController.cs | 6 +- .../DCL/Navmap/PlaceInfoPanelController.cs | 6 +- .../Assets/DCL/Navmap/SatelliteController.cs | 150 +++++++++--------- ...arePlacesAndEventsContextMenuController.cs | 6 +- .../NetworkDefinitions/Browser/IWebBrowser.cs | 13 -- .../Browser/IWebBrowser.cs.meta | 11 -- .../Browser/SupportRequestService.cs | 6 +- .../Browser/UnityAppWebBrowser.cs | 8 +- .../DCL/NftPrompt/NftPromptController.cs | 6 +- .../EquippedItems_PassportModuleController.cs | 8 +- .../UserBasicInfo_PassportModuleController.cs | 6 +- .../Assets/DCL/Passport/PassportController.cs | 4 +- .../PlacesCardSocialActionsController.cs | 6 +- .../Assets/DCL/Places/PlacesController.cs | 2 +- .../DCL/Places/PlacesResultsController.cs | 6 +- .../PluginSystem/Global/BackpackSubPlugin.cs | 4 +- .../PluginSystem/Global/CommunitiesPlugin.cs | 4 +- .../PluginSystem/Global/DonationsPlugin.cs | 4 +- .../PluginSystem/Global/ExplorePanelPlugin.cs | 4 +- .../Global/ExternalUrlPromptPlugin.cs | 4 +- .../PluginSystem/Global/FriendsContainer.cs | 2 +- .../DCL/PluginSystem/Global/GiftingPlugin.cs | 4 +- .../Global/MarketplaceCreditsPlugin.cs | 4 +- .../PluginSystem/Global/NftPromptPlugin.cs | 4 +- .../DCL/PluginSystem/Global/PassportPlugin.cs | 4 +- .../DCL/PluginSystem/Global/SidebarPlugin.cs | 4 +- .../Global/Web3AuthenticationPlugin.cs | 4 +- .../UI/ConfirmationDialog/ReportUserHelper.cs | 4 +- ...GenericUserProfileContextMenuController.cs | 4 +- .../Names/ProfileNameEditorController.cs | 8 +- .../DCL/UI/Profiles/ProfileMenuController.cs | 4 +- .../UI/Sidebar/HelpMenu/HelpMenuController.cs | 8 +- .../DCL/UI/Sidebar/SidebarController.cs | 8 +- .../UI/SystemMenu/SystemSectionPresenter.cs | 8 +- .../Dapp/DappDeepLinkAuthenticator.cs | 7 +- .../Dapp/DappWeb3EthereumApi.cs | 6 +- 77 files changed, 274 insertions(+), 299 deletions(-) delete mode 100644 Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs delete mode 100644 Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs.meta diff --git a/Explorer/Assets/DCL/ApplicationsGuards/ApplicationBlocklistGuard/BannedNotificationHandler.cs b/Explorer/Assets/DCL/ApplicationsGuards/ApplicationBlocklistGuard/BannedNotificationHandler.cs index a53353f8ea0..0c375eaaab7 100644 --- a/Explorer/Assets/DCL/ApplicationsGuards/ApplicationBlocklistGuard/BannedNotificationHandler.cs +++ b/Explorer/Assets/DCL/ApplicationsGuards/ApplicationBlocklistGuard/BannedNotificationHandler.cs @@ -20,7 +20,7 @@ public class BannedNotificationHandler : IDisposable private readonly IWeb3IdentityCache identityCache; private readonly ModerationDataProvider moderationDataProvider; private readonly IMVCManager mvcManager; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private CancellationTokenSource cts = new (); @@ -30,7 +30,7 @@ public BannedNotificationHandler( IWeb3IdentityCache identityCache, ModerationDataProvider moderationDataProvider, IMVCManager mvcManager, - IWebBrowser webBrowser) + UnityAppWebBrowser webBrowser) { this.webRequestController = webRequestController; this.urlsSource = urlsSource; @@ -47,7 +47,7 @@ public void Dispose() => cts.SafeCancelAndDispose(); private void OnBanWarningNotificationClicked(object[] parameters) => - webBrowser.OpenUrl(urlsSource.Url(DecentralandUrl.SupportLink)); + webBrowser.OpenUrlMainThreadOnly(urlsSource.Url(DecentralandUrl.SupportLink)); private void OnBannedNotificationClicked(object[] parameters) { diff --git a/Explorer/Assets/DCL/ApplicationsGuards/ApplicationBlocklistGuard/BlockedScreenController.cs b/Explorer/Assets/DCL/ApplicationsGuards/ApplicationBlocklistGuard/BlockedScreenController.cs index 6aae7a3dabe..f0c01a22733 100644 --- a/Explorer/Assets/DCL/ApplicationsGuards/ApplicationBlocklistGuard/BlockedScreenController.cs +++ b/Explorer/Assets/DCL/ApplicationsGuards/ApplicationBlocklistGuard/BlockedScreenController.cs @@ -14,12 +14,12 @@ public class BlockedScreenController : ControllerBase CanvasOrdering.SortingLayer.OVERLAY; - public BlockedScreenController(ViewFactoryMethod viewFactory, IWebBrowser webBrowser) : base(viewFactory) + public BlockedScreenController(ViewFactoryMethod viewFactory, UnityAppWebBrowser webBrowser) : base(viewFactory) { this.webBrowser = webBrowser; } @@ -66,7 +66,7 @@ public override void Dispose() private void OnSupportClicked() { - webBrowser.OpenUrl(DecentralandUrl.Help); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.Help); } private static string FormatRemainingBanTime(string expiresAt) diff --git a/Explorer/Assets/DCL/ApplicationsGuards/ApplicationMinimumSpecsGuard/UI/MinimumSpecsScreenController.cs b/Explorer/Assets/DCL/ApplicationsGuards/ApplicationMinimumSpecsGuard/UI/MinimumSpecsScreenController.cs index 78766b74b2e..d8e35d13121 100644 --- a/Explorer/Assets/DCL/ApplicationsGuards/ApplicationMinimumSpecsGuard/UI/MinimumSpecsScreenController.cs +++ b/Explorer/Assets/DCL/ApplicationsGuards/ApplicationMinimumSpecsGuard/UI/MinimumSpecsScreenController.cs @@ -12,7 +12,7 @@ namespace DCL.ApplicationMinimumSpecsGuard { public class MinimumSpecsScreenController : ControllerBase { - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IAnalyticsController analytics; private readonly IReadOnlyList specResult; public override CanvasOrdering.SortingLayer Layer => CanvasOrdering.SortingLayer.OVERLAY; @@ -21,7 +21,7 @@ public class MinimumSpecsScreenController : ControllerBase specResult) : base(viewFactory) { @@ -67,7 +67,7 @@ private void OnContinueClicked() private void OnReadMoreClicked() { - webBrowser.OpenUrl(DecentralandUrl.MinimumSpecs); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.MinimumSpecs); } protected override UniTask WaitForCloseIntentAsync(CancellationToken ct) => diff --git a/Explorer/Assets/DCL/ApplicationsGuards/ApplicationVersionGuard/ApplicationVersionGuard.cs b/Explorer/Assets/DCL/ApplicationsGuards/ApplicationVersionGuard/ApplicationVersionGuard.cs index e31f570d6a2..c95f7d8afb1 100644 --- a/Explorer/Assets/DCL/ApplicationsGuards/ApplicationVersionGuard/ApplicationVersionGuard.cs +++ b/Explorer/Assets/DCL/ApplicationsGuards/ApplicationVersionGuard/ApplicationVersionGuard.cs @@ -31,9 +31,9 @@ public class ApplicationVersionGuard private const string DECENTRALAND_LEGACY_LAUNCHER_MAC_X_64DMG = "Decentraland Outdated-mac-x64.dmg"; private readonly IWebRequestController webRequestController; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; - public ApplicationVersionGuard(IWebRequestController webRequestController, IWebBrowser webBrowser) + public ApplicationVersionGuard(IWebRequestController webRequestController, UnityAppWebBrowser webBrowser) { this.webRequestController = webRequestController; this.webBrowser = webBrowser; @@ -89,7 +89,7 @@ private void DownloadLauncher() string downloadUrl = $"{GetLauncherDownloadPath()}/{assetName}"; if (!string.IsNullOrEmpty(downloadUrl)) - webBrowser.OpenUrl(downloadUrl); + webBrowser.OpenUrlMainThreadOnly(downloadUrl); else ReportHub.LogError(ReportCategory.VERSION_CONTROL, "Failed to get launcher download URL."); } diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs index ac5e0d2f811..f066b1ffbe0 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs @@ -52,7 +52,7 @@ public enum AuthStatus private readonly ICompositeWeb3Provider web3Authenticator; private readonly ISelfProfile selfProfile; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IWeb3IdentityCache storedIdentityProvider; private readonly ICharacterPreviewFactory characterPreviewFactory; private readonly SplashScreen splashScreen; @@ -99,7 +99,7 @@ public AuthenticationScreenController( ViewFactoryMethod viewFactory, ICompositeWeb3Provider web3Authenticator, ISelfProfile selfProfile, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IWeb3IdentityCache storedIdentityProvider, ICharacterPreviewFactory characterPreviewFactory, SplashScreen splashScreen, @@ -289,7 +289,7 @@ async UniTaskVoid ChangeAccountAsync(CancellationToken ct) private void OpenSupportUrl() { - webBrowser.OpenUrl(DecentralandUrl.SupportLink); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.SupportLink); DiscordButtonClicked?.Invoke(); } diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LobbyForNewAccountAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LobbyForNewAccountAuthState.cs index 9ffe2346679..02e25641e2d 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LobbyForNewAccountAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LobbyForNewAccountAuthState.cs @@ -33,7 +33,7 @@ public class LobbyForNewAccountAuthState : AuthStateBase, IPayloadedState<(Profi private readonly LobbyForNewAccountAuthView view; private readonly IWearablesProvider wearablesProvider; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IWebRequestController webRequestController; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly ProfileChangesBus profileChangesBus; @@ -57,7 +57,7 @@ public LobbyForNewAccountAuthState(MVCStateMachine fsm, AuthenticationScreenCharacterPreviewController characterPreviewController, ISelfProfile selfProfile, IWearablesProvider wearablesProvider, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IWebRequestController webRequestController, IDecentralandUrlsSource decentralandUrlsSource, ProfileChangesBus profileChangesBus) : base(viewInstance) @@ -161,7 +161,7 @@ private void ReparentCharacterPreview() } private void OpenClickableURL(string url) => - webBrowser.OpenUrl(url); + webBrowser.OpenUrlMainThreadOnly(url); private async UniTask InitializeAvatarAsync() { diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs index 06a651cb3e8..62f0a63bc78 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/LoginSelectionAuthState.cs @@ -22,13 +22,13 @@ public class LoginSelectionAuthState : AuthStateBase, IState, IPayloadedState currentState; private readonly SplashScreen splashScreen; private readonly ICompositeWeb3Provider compositeWeb3Provider; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly bool enableEmailOTP; public LoginSelectionAuthState(MVCStateMachine machine, AuthenticationScreenView viewInstance, AuthenticationScreenController controller, ReactiveProperty currentState, SplashScreen splashScreen, - ICompositeWeb3Provider compositeWeb3Provider, IWebBrowser webBrowser, bool enableEmailOTP) : base(viewInstance) + ICompositeWeb3Provider compositeWeb3Provider, UnityAppWebBrowser webBrowser, bool enableEmailOTP) : base(viewInstance) { view = viewInstance.LoginSelectionAuthView; @@ -202,7 +202,7 @@ private void CloseErrorPopup() => view.ErrorPopupRoot.SetActive(false); private void RequestAlphaAccess() => - webBrowser.OpenUrl(REQUEST_BETA_ACCESS_LINK); + webBrowser.OpenUrlMainThreadOnly(REQUEST_BETA_ACCESS_LINK); } public enum ErrorType diff --git a/Explorer/Assets/DCL/Backpack/AvatarSection/AvatarController.cs b/Explorer/Assets/DCL/Backpack/AvatarSection/AvatarController.cs index 54d7b5886d4..97eae67be80 100644 --- a/Explorer/Assets/DCL/Backpack/AvatarSection/AvatarController.cs +++ b/Explorer/Assets/DCL/Backpack/AvatarSection/AvatarController.cs @@ -15,7 +15,7 @@ public class AvatarController : ISection, IDisposable { private readonly AvatarView view; private readonly RectTransform rectTransform; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly BackpackSlotsController slotsController; private readonly CategoriesPresenter categoriesPresenter; private readonly OutfitsPresenter outfitsPresenter; @@ -28,7 +28,7 @@ public class AvatarController : ISection, IDisposable private readonly URLParameter marketplaceSourceParam = new ("utm_source", "backpack"); public AvatarController(AvatarView view, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, AvatarSlotView[] slotViews, NftTypeIconSO rarityBackgrounds, BackpackCommandBus backpackCommandBus, @@ -77,7 +77,7 @@ private void OnOpenMarketplace() urlBuilder.Clear(); urlBuilder.AppendDomain(URLDomain.FromString(decentralandUrlsSource.Url(DecentralandUrl.Market))); urlBuilder.AppendParameter(marketplaceSourceParam); - webBrowser.OpenUrl(urlBuilder.Build()); + webBrowser.OpenUrlMainThreadOnly(urlBuilder.Build()); } public void Dispose() diff --git a/Explorer/Assets/DCL/Backpack/AvatarSection/Outfits/OutfitsPresenter.cs b/Explorer/Assets/DCL/Backpack/AvatarSection/Outfits/OutfitsPresenter.cs index d4bc9e0c5df..253a586486d 100644 --- a/Explorer/Assets/DCL/Backpack/AvatarSection/Outfits/OutfitsPresenter.cs +++ b/Explorer/Assets/DCL/Backpack/AvatarSection/Outfits/OutfitsPresenter.cs @@ -33,7 +33,7 @@ public class OutfitsPresenter : ISection, IDisposable private readonly IEventBus eventBus; private readonly IBackpackEventBus backpackEventBus; private readonly IEquippedWearables equippedWearables; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly OutfitApplier outfitApplier; private readonly OutfitBannerPresenter outfitBannerPresenter; private readonly OutfitsCollection outfitsCollection; @@ -58,7 +58,7 @@ public OutfitsPresenter(OutfitsView view, IBackpackEventBus backpackEventBus, OutfitApplier outfitApplier, OutfitsCollection outfitsCollection, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IEquippedWearables equippedWearables, LoadOutfitsCommand loadOutfitsCommand, SaveOutfitCommand saveOutfitCommand, @@ -519,13 +519,13 @@ private void UpdateFirstEmptySlotPrompt() private void OnLinkClicked(string url) { - webBrowser.OpenUrl(url); + webBrowser.OpenUrlMainThreadOnly(url); } private void OnGetANameClicked() { - webBrowser.OpenUrl("https://decentraland.org/marketplace/names/claim"); + webBrowser.OpenUrlMainThreadOnly("https://decentraland.org/marketplace/names/claim"); eventBus.Publish(new OutfitsEvents.ClaimExtraOutfitsEvent()); } diff --git a/Explorer/Assets/DCL/Backpack/BackpackController.cs b/Explorer/Assets/DCL/Backpack/BackpackController.cs index 305bb1a2552..8a8881b1e15 100644 --- a/Explorer/Assets/DCL/Backpack/BackpackController.cs +++ b/Explorer/Assets/DCL/Backpack/BackpackController.cs @@ -63,7 +63,7 @@ public class BackpackController : ISection, IDisposable public BackpackController( BackpackView view, ISelfProfile selfProfile, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, AvatarView avatarView, NftTypeIconSO rarityInfoPanelBackgrounds, BackpackCommandBus backpackCommandBus, diff --git a/Explorer/Assets/DCL/Backpack/BackpackGridController.cs b/Explorer/Assets/DCL/Backpack/BackpackGridController.cs index 50d306a43c8..1a9356edb53 100644 --- a/Explorer/Assets/DCL/Backpack/BackpackGridController.cs +++ b/Explorer/Assets/DCL/Backpack/BackpackGridController.cs @@ -43,7 +43,7 @@ public class BackpackGridController : IDisposable private readonly IObjectPool gridItemsPool; private readonly IThumbnailProvider thumbnailProvider; private readonly IWearablesProvider wearablesProvider; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IWearableStorage wearableStorage; private readonly SmartWearableCache smartWearableCache; private readonly IMVCManager mvcManager; @@ -77,7 +77,7 @@ public BackpackGridController(BackpackGridView view, IObjectPool gridItemsPool, IThumbnailProvider thumbnailProvider, IWearablesProvider wearablesProvider, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, ColorToggleView colorToggle, ColorPresetsSO hairColors, ColorPresetsSO eyesColors, @@ -523,6 +523,6 @@ private void UpdateBodyShapeCompatibility(IReadOnlyList wearab } private void OpenMarketplaceLink(string url) => - webBrowser.OpenUrl(url); + webBrowser.OpenUrlMainThreadOnly(url); } } diff --git a/Explorer/Assets/DCL/Backpack/EmotesSection/BackpackEmoteGridController.cs b/Explorer/Assets/DCL/Backpack/EmotesSection/BackpackEmoteGridController.cs index 720fe230b76..39fa8286df8 100644 --- a/Explorer/Assets/DCL/Backpack/EmotesSection/BackpackEmoteGridController.cs +++ b/Explorer/Assets/DCL/Backpack/EmotesSection/BackpackEmoteGridController.cs @@ -47,7 +47,7 @@ public class BackpackEmoteGridController : IDisposable private readonly IObjectPool gridItemsPool; private readonly IEmoteProvider emoteProvider; private readonly IThumbnailProvider thumbnailProvider; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IEmoteStorage emoteStorage; private readonly IPendingTransferService ownedNftFilter; @@ -71,7 +71,7 @@ public BackpackEmoteGridController( IObjectPool gridItemsPool, IEmoteProvider emoteProvider, IThumbnailProvider thumbnailProvider, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IEmoteStorage emoteStorage, IPendingTransferService ownedNftFilter) { @@ -459,6 +459,6 @@ private void OnWearableEquipped(IWearable wearable, bool isManuallyEquipped) } private void OpenMarketplaceLink(string url) => - webBrowser.OpenUrl(url); + webBrowser.OpenUrlMainThreadOnly(url); } } diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftTransfer/GiftTransferController.cs b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftTransfer/GiftTransferController.cs index cfbbeb42263..a86f9dfcffe 100644 --- a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftTransfer/GiftTransferController.cs +++ b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftTransfer/GiftTransferController.cs @@ -30,7 +30,7 @@ private enum State { Waiting, Success, Failed } private State currentState; private readonly IEventBus eventBus; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IMVCManager mvcManager; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly GiftTransferRequestCommand giftTransferRequestCommand; @@ -41,7 +41,7 @@ private enum State { Waiting, Success, Failed } private CancellationTokenSource? delayCts; public GiftTransferController(ViewFactoryMethod viewFactory, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IEventBus eventBus, IMVCManager mvcManager, IDecentralandUrlsSource decentralandUrlsSource, @@ -292,7 +292,7 @@ private async UniTask ShowErrorPopupAsync(CancellationToken ct) private void LinkCallback(string url) { - webBrowser.OpenUrl(url); + webBrowser.OpenUrlMainThreadOnly(url); } } } diff --git a/Explorer/Assets/DCL/Communities/CommunitiesBrowser/CommunitiesBrowserController.cs b/Explorer/Assets/DCL/Communities/CommunitiesBrowser/CommunitiesBrowserController.cs index b86b58ba07c..864779f155c 100644 --- a/Explorer/Assets/DCL/Communities/CommunitiesBrowser/CommunitiesBrowserController.cs +++ b/Explorer/Assets/DCL/Communities/CommunitiesBrowser/CommunitiesBrowserController.cs @@ -63,7 +63,7 @@ public class CommunitiesBrowserController : ISection, IDisposable private readonly IAnalyticsController analytics; private readonly CommunityDataService communityDataService; private readonly ILoadingStatus loadingStatus; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly CommunitiesBrowserMyCommunitiesPresenter myCommunitiesPresenter; @@ -101,7 +101,7 @@ public CommunitiesBrowserController( IAnalyticsController analytics, CommunityDataService communityDataService, ILoadingStatus loadingStatus, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource) { this.view = view; diff --git a/Explorer/Assets/DCL/Communities/CommunitiesCard/CommunityCardController.cs b/Explorer/Assets/DCL/Communities/CommunitiesCard/CommunityCardController.cs index 57ccc9c9eb7..2f2531e8c6c 100644 --- a/Explorer/Assets/DCL/Communities/CommunitiesCard/CommunityCardController.cs +++ b/Explorer/Assets/DCL/Communities/CommunitiesCard/CommunityCardController.cs @@ -77,7 +77,7 @@ public class CommunityCardController : ControllerBase eventsFetchData = new (PAGE_SIZE); @@ -58,7 +58,7 @@ public EventListController(EventListView view, ThumbnailLoader thumbnailLoader, IMVCManager mvcManager, ISystemClipboard clipboard, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IRealmNavigator realmNavigator, IDecentralandUrlsSource decentralandUrlsSource) : base(view, PAGE_SIZE) { @@ -101,7 +101,7 @@ public override void Dispose() } private void OnCreateEventButtonClicked() => - webBrowser.OpenUrl(string.Format(createEventFormat, communityData?.id)); + webBrowser.OpenUrlMainThreadOnly(string.Format(createEventFormat, communityData?.id)); private void OnEventCopyLinkButtonClicked(PlaceAndEventDTO eventData) { @@ -111,7 +111,7 @@ private void OnEventCopyLinkButtonClicked(PlaceAndEventDTO eventData) } private void OnEventShareButtonClicked(PlaceAndEventDTO eventData) => - webBrowser.OpenUrl($"{EventUtilities.GetEventShareLink(eventData.Event, decentralandUrlsSource)}&utm_source=explorer&utm_campaign=communities"); + webBrowser.OpenUrlMainThreadOnly($"{EventUtilities.GetEventShareLink(eventData.Event, decentralandUrlsSource)}&utm_source=explorer&utm_campaign=communities"); private void OnInterestedButtonClicked(PlaceAndEventDTO eventData, EventListItemView eventItemView) { diff --git a/Explorer/Assets/DCL/Communities/CommunitiesCard/Members/MembersListController.cs b/Explorer/Assets/DCL/Communities/CommunitiesCard/Members/MembersListController.cs index 600b2becb4b..8318b68889e 100644 --- a/Explorer/Assets/DCL/Communities/CommunitiesCard/Members/MembersListController.cs +++ b/Explorer/Assets/DCL/Communities/CommunitiesCard/Members/MembersListController.cs @@ -51,7 +51,7 @@ public class MembersListController : CommunityFetchingControllerBase> sectionsFetchData = new (); @@ -88,7 +88,7 @@ public MembersListController(MembersListView view, ChatEventBus chatEventBus, IWeb3IdentityCache web3IdentityCache, ISelfProfile selfProfile, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource) : base(view, PAGE_SIZE) { this.view = view; diff --git a/Explorer/Assets/DCL/Communities/CommunitiesCard/Places/PlacesSectionController.cs b/Explorer/Assets/DCL/Communities/CommunitiesCard/Places/PlacesSectionController.cs index 7574c6b895a..6bead89fdc9 100644 --- a/Explorer/Assets/DCL/Communities/CommunitiesCard/Places/PlacesSectionController.cs +++ b/Explorer/Assets/DCL/Communities/CommunitiesCard/Places/PlacesSectionController.cs @@ -69,7 +69,7 @@ public PlacesSectionController(PlacesSectionView view, IRealmNavigator realmNavigator, IMVCManager mvcManager, ISystemClipboard clipboard, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IProfileRepository profileRepository, IDecentralandUrlsSource dclUrlSource, HomePlaceEventBus homePlaceEventBus, diff --git a/Explorer/Assets/DCL/Communities/CommunityCreation/CommunityCreationEditionController.cs b/Explorer/Assets/DCL/Communities/CommunityCreation/CommunityCreationEditionController.cs index a4d28e2bad9..9383b82f506 100644 --- a/Explorer/Assets/DCL/Communities/CommunityCreation/CommunityCreationEditionController.cs +++ b/Explorer/Assets/DCL/Communities/CommunityCreation/CommunityCreationEditionController.cs @@ -47,7 +47,7 @@ public class CommunityCreationEditionController : ControllerBase - webBrowser.OpenUrl(DecentralandUrl.MarketplaceClaimName); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.MarketplaceClaimName); private void DisableShortcutsInput() => inputBlock.Disable(InputMapComponent.Kind.SHORTCUTS, InputMapComponent.Kind.IN_WORLD_CAMERA); @@ -615,10 +615,10 @@ private void OpenContentPolicyAndCodeOfEthicsLink(string id) switch (id) { case CONTENT_POLICY_LINK_ID: - webBrowser.OpenUrl(DecentralandUrl.ContentPolicy); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.ContentPolicy); break; case CODE_AND_ETHICS_LINK_ID: - webBrowser.OpenUrl(DecentralandUrl.CodeOfEthics); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.CodeOfEthics); break; } diff --git a/Explorer/Assets/DCL/Donations/UI/DonationsPanelController.cs b/Explorer/Assets/DCL/Donations/UI/DonationsPanelController.cs index 3c04706f21e..ac39fe8c206 100644 --- a/Explorer/Assets/DCL/Donations/UI/DonationsPanelController.cs +++ b/Explorer/Assets/DCL/Donations/UI/DonationsPanelController.cs @@ -35,7 +35,7 @@ public class DonationsPanelController : ControllerBase - webBrowser.OpenUrl(decentralandUrlsSource.Url(DecentralandUrl.Account)); + webBrowser.OpenUrlMainThreadOnly(decentralandUrlsSource.Url(DecentralandUrl.Account)); private void OnContactSupportRequested() => - webBrowser.OpenUrl(decentralandUrlsSource.Url(DecentralandUrl.Help)); + webBrowser.OpenUrlMainThreadOnly(decentralandUrlsSource.Url(DecentralandUrl.Help)); protected override void OnBeforeViewShow() { diff --git a/Explorer/Assets/DCL/Events/EventCardActionsController.cs b/Explorer/Assets/DCL/Events/EventCardActionsController.cs index d08366f9990..fbba43d67e7 100644 --- a/Explorer/Assets/DCL/Events/EventCardActionsController.cs +++ b/Explorer/Assets/DCL/Events/EventCardActionsController.cs @@ -29,14 +29,14 @@ public class EventCardActionsController private const string LINK_COPIED_MESSAGE = "Link copied to clipboard!"; private readonly HttpEventsApiService eventsApiService; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IRealmNavigator realmNavigator; private readonly ISystemClipboard clipboard; private readonly IDecentralandUrlsSource decentralandUrlsSource; public EventCardActionsController( HttpEventsApiService eventsApiService, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IRealmNavigator realmNavigator, ISystemClipboard clipboard, IDecentralandUrlsSource decentralandUrlsSource) @@ -74,13 +74,13 @@ public async UniTaskVoid SetEventAsInterestedAsync(IEventDTO eventData, EventCar public void AddEventToCalendar(IEventDTO eventData) { - webBrowser.OpenUrl($"{EventUtilities.GetEventAddToCalendarLink(eventData, decentralandUrlsSource)}&utm_source=explorer&utm_campaign=discover"); + webBrowser.OpenUrlMainThreadOnly($"{EventUtilities.GetEventAddToCalendarLink(eventData, decentralandUrlsSource)}&utm_source=explorer&utm_campaign=discover"); AddEventToCalendarClicked?.Invoke(eventData); } public void AddEventToCalendar(IEventDTO eventData, DateTime utcStart) { - webBrowser.OpenUrl($"{EventUtilities.GetEventAddToCalendarLink(eventData, utcStart, decentralandUrlsSource)}&utm_source=explorer&utm_campaign=discover"); + webBrowser.OpenUrlMainThreadOnly($"{EventUtilities.GetEventAddToCalendarLink(eventData, utcStart, decentralandUrlsSource)}&utm_source=explorer&utm_campaign=discover"); AddEventToCalendarClicked?.Invoke(eventData); } @@ -104,7 +104,7 @@ public void JumpInEvent(IEventDTO eventData, CancellationToken ct) public void ShareEvent(IEventDTO eventData) { - webBrowser.OpenUrl($"{EventUtilities.GetEventShareLink(eventData, decentralandUrlsSource)}&utm_source=explorer&utm_campaign=discover"); + webBrowser.OpenUrlMainThreadOnly($"{EventUtilities.GetEventShareLink(eventData, decentralandUrlsSource)}&utm_source=explorer&utm_campaign=discover"); EventShared?.Invoke(eventData); } diff --git a/Explorer/Assets/DCL/Events/EventDetailPanelController.cs b/Explorer/Assets/DCL/Events/EventDetailPanelController.cs index deb71fcdfa1..bc9235ff2f8 100644 --- a/Explorer/Assets/DCL/Events/EventDetailPanelController.cs +++ b/Explorer/Assets/DCL/Events/EventDetailPanelController.cs @@ -25,7 +25,7 @@ public class EventDetailPanelController : ControllerBase CanvasOrdering.SortingLayer.POPUP; @@ -37,7 +37,7 @@ public class EventDetailPanelController : ControllerBase public void GoToCreateEventPage(bool fromHeader) { - webBrowser.OpenUrl($"{decentralandUrlsSource.Url(DecentralandUrl.WhatsOnNewEventLink)}?utm_source=explorer&utm_campaign=discover"); + webBrowser.OpenUrlMainThreadOnly($"{decentralandUrlsSource.Url(DecentralandUrl.WhatsOnNewEventLink)}?utm_source=explorer&utm_campaign=discover"); CreateEventButtonClicked?.Invoke(fromHeader); } } diff --git a/Explorer/Assets/DCL/EventsApi/GoogleUserCalendar.cs b/Explorer/Assets/DCL/EventsApi/GoogleUserCalendar.cs index 65fd8190947..aecf3bdeb1e 100644 --- a/Explorer/Assets/DCL/EventsApi/GoogleUserCalendar.cs +++ b/Explorer/Assets/DCL/EventsApi/GoogleUserCalendar.cs @@ -13,10 +13,10 @@ public class GoogleUserCalendar private const string DATES_PARAM = "dates"; private const string DESCRIPTION_PARAM = "details"; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly URLBuilder urlBuilder = new (); - public GoogleUserCalendar(IWebBrowser webBrowser) + public GoogleUserCalendar(UnityAppWebBrowser webBrowser) { this.webBrowser = webBrowser; } @@ -31,7 +31,7 @@ public void Add(string title, string description, DateTime startAt, DateTime end .AppendParameter(new URLParameter(DATES_PARAM, $"{startAt:yyyyMMddTHHmmssZ}/{endAt:yyyyMMddTHHmmssZ}")) .AppendParameter(new URLParameter(DESCRIPTION_PARAM, description)); - webBrowser.OpenUrl(urlBuilder.Build()); + webBrowser.OpenUrlMainThreadOnly(urlBuilder.Build()); } } } diff --git a/Explorer/Assets/DCL/ExternalUrlPrompt/ExternalUrlPromptController.cs b/Explorer/Assets/DCL/ExternalUrlPrompt/ExternalUrlPromptController.cs index fade72b9c14..c2b9f1304b2 100644 --- a/Explorer/Assets/DCL/ExternalUrlPrompt/ExternalUrlPromptController.cs +++ b/Explorer/Assets/DCL/ExternalUrlPrompt/ExternalUrlPromptController.cs @@ -12,14 +12,14 @@ public partial class ExternalUrlPromptController : ControllerBase CanvasOrdering.SortingLayer.POPUP; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly ICursor cursor; private readonly List trustedDomains = new (); private Action resultCallback; public ExternalUrlPromptController( ViewFactoryMethod viewFactory, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, ICursor cursor) : base(viewFactory) { this.webBrowser = webBrowser; @@ -40,7 +40,7 @@ protected override void OnViewShow() if (trustedDomains.Contains(inputData.Uri.Host)) { - webBrowser.OpenUrl(inputData.Uri.OriginalString); + webBrowser.OpenUrlMainThreadOnly(inputData.Uri.OriginalString); viewInstance.CloseButton.OnClickAsync(CancellationToken.None).Forget(); return; } @@ -53,10 +53,10 @@ protected override void OnViewShow() case ExternalUrlPromptResultType.ApprovedTrusted: if (!trustedDomains.Contains(inputData.Uri.Host)) trustedDomains.Add(inputData.Uri.Host); - webBrowser.OpenUrl(inputData.Uri.OriginalString); + webBrowser.OpenUrlMainThreadOnly(inputData.Uri.OriginalString); break; case ExternalUrlPromptResultType.Approved: - webBrowser.OpenUrl(inputData.Uri.OriginalString); + webBrowser.OpenUrlMainThreadOnly(inputData.Uri.OriginalString); break; } }); diff --git a/Explorer/Assets/DCL/Friends/UI/FriendPanel/FriendsPanelController.cs b/Explorer/Assets/DCL/Friends/UI/FriendPanel/FriendsPanelController.cs index 77f1461c43c..2b38c7fbb7e 100644 --- a/Explorer/Assets/DCL/Friends/UI/FriendPanel/FriendsPanelController.cs +++ b/Explorer/Assets/DCL/Friends/UI/FriendPanel/FriendsPanelController.cs @@ -63,7 +63,7 @@ public FriendsPanelController(ViewFactoryMethod viewFactory, FriendsConnectivityStatusTracker friendsConnectivityStatusTracker, ProfileRepositoryWrapper profileDataProvider, IVoiceChatOrchestrator voiceChatOrchestrator, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource, ISelfProfile selfProfile) : base(viewFactory) { diff --git a/Explorer/Assets/DCL/Friends/UI/FriendPanel/Sections/Requests/RequestsSectionController.cs b/Explorer/Assets/DCL/Friends/UI/FriendPanel/Sections/Requests/RequestsSectionController.cs index f7432aa9d8f..3534b3d3b00 100644 --- a/Explorer/Assets/DCL/Friends/UI/FriendPanel/Sections/Requests/RequestsSectionController.cs +++ b/Explorer/Assets/DCL/Friends/UI/FriendPanel/Sections/Requests/RequestsSectionController.cs @@ -28,7 +28,7 @@ public class RequestsSectionController : FriendPanelSectionDoubleCollectionContr private readonly GenericContextMenu contextMenu; private readonly UserProfileContextMenuControlSettings userProfileContextMenuControlSettings; private readonly IPassportBridge passportBridge; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly ISelfProfile selfProfile; @@ -45,7 +45,7 @@ public RequestsSectionController(RequestsSectionView view, RequestsRequestManager requestManager, IPassportBridge passportBridge, bool includeUserBlocking, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource, ISelfProfile selfProfile) : base(view, friendsService, friendEventBus, mvcManager, requestManager) diff --git a/Explorer/Assets/DCL/InWorldCamera/CameraReelGallery/CameraReelGalleryController.cs b/Explorer/Assets/DCL/InWorldCamera/CameraReelGallery/CameraReelGalleryController.cs index 1727c04b41d..756d80f8d2b 100644 --- a/Explorer/Assets/DCL/InWorldCamera/CameraReelGallery/CameraReelGalleryController.cs +++ b/Explorer/Assets/DCL/InWorldCamera/CameraReelGallery/CameraReelGalleryController.cs @@ -68,7 +68,7 @@ public delegate void ThumbnailClick( private readonly ICameraReelStorageService cameraReelStorageService; private readonly IDecentralandUrlsSource? decentralandUrlsSource; private readonly ISystemClipboard? systemClipboard; - private readonly IWebBrowser? webBrowser; + private readonly UnityAppWebBrowser? webBrowser; private readonly GalleryEventBus galleryEventBus; private readonly ReelGalleryPoolManager reelGalleryPoolManager; private readonly Dictionary monthViews = new (); @@ -100,7 +100,7 @@ public CameraReelGalleryController(CameraReelGalleryView view, bool useSignedRequest, GalleryEventBus galleryEventBus, CameraReelOptionButtonView? optionButtonView = null, - IWebBrowser? webBrowser = null, + UnityAppWebBrowser? webBrowser = null, IDecentralandUrlsSource? decentralandUrlsSource = null, ISystemClipboard? systemClipboard = null, CameraReelGalleryMessagesConfiguration? reelGalleryStringMessages = null, diff --git a/Explorer/Assets/DCL/InWorldCamera/InWorldCamera/Systems/InWorldCameraPlugin.cs b/Explorer/Assets/DCL/InWorldCamera/InWorldCamera/Systems/InWorldCameraPlugin.cs index 7a9cc9346bc..800a9632f7a 100644 --- a/Explorer/Assets/DCL/InWorldCamera/InWorldCamera/Systems/InWorldCameraPlugin.cs +++ b/Explorer/Assets/DCL/InWorldCamera/InWorldCamera/Systems/InWorldCameraPlugin.cs @@ -59,7 +59,7 @@ public class InWorldCameraPlugin : IDCLGlobalPlugin private readonly IMVCManager mvcManager; private readonly ISystemClipboard systemClipboard; private readonly IDecentralandUrlsSource decentralandUrlsSource; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IProfileRepository profileRepository; private readonly IRealmNavigator realmNavigator; private readonly IWearableStorage wearableStorage; @@ -87,7 +87,7 @@ public InWorldCameraPlugin(SelfProfile selfProfile, RealmData realmData, Entity playerEntity, IPlacesAPIService placesAPIService, ICharacterObject characterObject, ICoroutineRunner coroutineRunner, ICameraReelStorageService cameraReelStorageService, ICameraReelScreenshotsStorage cameraReelScreenshotsStorage, IMVCManager mvcManager, - ISystemClipboard systemClipboard, IDecentralandUrlsSource decentralandUrlsSource, IWebBrowser webBrowser, + ISystemClipboard systemClipboard, IDecentralandUrlsSource decentralandUrlsSource, UnityAppWebBrowser webBrowser, IProfileRepository profileRepository, IRealmNavigator realmNavigator, IAssetsProvisioner assetsProvisioner, IWearableStorage wearableStorage, IWearablesProvider wearablesProvider, diff --git a/Explorer/Assets/DCL/InWorldCamera/PhotoDetail/EquippedWearableController.cs b/Explorer/Assets/DCL/InWorldCamera/PhotoDetail/EquippedWearableController.cs index 87793d02475..6c50148eb8f 100644 --- a/Explorer/Assets/DCL/InWorldCamera/PhotoDetail/EquippedWearableController.cs +++ b/Explorer/Assets/DCL/InWorldCamera/PhotoDetail/EquippedWearableController.cs @@ -18,7 +18,7 @@ public class EquippedWearableController : IDisposable { internal event Action MarketClicked; internal readonly EquippedWearableView view; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly IThumbnailProvider thumbnailProvider; private readonly NftTypeIconSO rarityBackgrounds; @@ -28,7 +28,7 @@ public class EquippedWearableController : IDisposable private IWearable currentWearable; public EquippedWearableController(EquippedWearableView view, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource, IThumbnailProvider thumbnailProvider, NftTypeIconSO rarityBackgrounds, @@ -63,7 +63,7 @@ private void BuyWearableButtonClicked() async UniTaskVoid AnimateAndAwaitAsync() { await UniTask.Delay((int)(view.buyButtonAnimationDuration * 1000)); - webBrowser.OpenUrl(currentWearable.GetMarketplaceLink(decentralandUrlsSource)); + webBrowser.OpenUrlMainThreadOnly(currentWearable.GetMarketplaceLink(decentralandUrlsSource)); MarketClicked?.Invoke(); } diff --git a/Explorer/Assets/DCL/InWorldCamera/PhotoDetail/PhotoDetailController.cs b/Explorer/Assets/DCL/InWorldCamera/PhotoDetail/PhotoDetailController.cs index 6a4dc502c25..d13e61a6b1e 100644 --- a/Explorer/Assets/DCL/InWorldCamera/PhotoDetail/PhotoDetailController.cs +++ b/Explorer/Assets/DCL/InWorldCamera/PhotoDetail/PhotoDetailController.cs @@ -36,7 +36,7 @@ public class PhotoDetailController : ControllerBase - public static void ShareReelToX(string shareToXMessage, string reelId, IDecentralandUrlsSource decentralandUrlsSource, ISystemClipboard systemClipboard, IWebBrowser webBrowser) + public static void ShareReelToX(string shareToXMessage, string reelId, IDecentralandUrlsSource decentralandUrlsSource, ISystemClipboard systemClipboard, UnityAppWebBrowser webBrowser) { string description = shareToXMessage; string url = $"{decentralandUrlsSource.Url(DecentralandUrl.CameraReelLink)}/{reelId}"; @@ -26,7 +26,7 @@ public static void ShareReelToX(string shareToXMessage, string reelId, IDecentra string xUrl = $"https://x.com/intent/post?text={description}&url={url}"; systemClipboard.Set(xUrl); - webBrowser.OpenUrl(xUrl); + webBrowser.OpenUrlMainThreadOnly(xUrl); } /// diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index 95983c4289e..2c677507f7e 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -43,7 +43,7 @@ public class BootstrapContainer : DCLGlobalContainer public bool EnableAnalytics => Analytics.Enabled; public DiagnosticsContainer DiagnosticsContainer { get; private set; } public IDecentralandUrlsSource DecentralandUrlsSource { get; private set; } - public IWebBrowser WebBrowser { get; private set; } + public UnityAppWebBrowser WebBrowser { get; private set; } public IWeb3AccountFactory Web3AccountFactory { get; private set; } public IAssetsProvisioner? AssetsProvisioner { get; private init; } public IBootstrap? Bootstrap { get; private set; } @@ -181,7 +181,7 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies( DynamicSceneLoaderSettings sceneLoaderSettings, IWeb3AccountFactory web3AccountFactory, IWeb3IdentityCache identityCache, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, AnalyticsContainer container, IDecentralandUrlsSource decentralandUrlsSource, DecentralandEnvironment dclEnvironment, diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs index dd3a155d69e..6044583bfab 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -225,7 +225,7 @@ public override void Dispose() var nftInfoAPIClient = new OpenSeaAPIClient(staticContainer.WebRequestsContainer.WebRequestController, bootstrapContainer.DecentralandUrlsSource); var characterPreviewFactory = new CharacterPreviewFactory(staticContainer.ComponentsContainer.ComponentPoolsRegistry, appArgs); - IWebBrowser webBrowser = bootstrapContainer.WebBrowser; + UnityAppWebBrowser webBrowser = bootstrapContainer.WebBrowser; IEmoteStorage emotesCache = staticContainer.EmoteStorage; diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/MainSceneLoader.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/MainSceneLoader.cs index 91804ee6d5b..61df8e97e03 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/MainSceneLoader.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/MainSceneLoader.cs @@ -458,7 +458,7 @@ private bool ShouldForceSingleRunningInstance(IAppArgs appArgs) return false; } - private async UniTask RegisterBlockedPopupAsync(IWebBrowser webBrowser, CancellationToken ct) + private async UniTask RegisterBlockedPopupAsync(UnityAppWebBrowser webBrowser, CancellationToken ct) { var blockedPopupPrefab = await bootstrapContainer!.AssetsProvisioner!.ProvideMainAssetAsync(dynamicSettings.BlockedScreenPrefab, ct); @@ -469,7 +469,7 @@ private async UniTask RegisterBlockedPopupAsync(IWebBrowser webBrowser, Cancella dynamicWorldContainer!.MvcManager.RegisterController(launcherRedirectionScreenController); } - private async UniTask> VerifyMinimumHardwareRequirementMetAsync(IAppArgs applicationParametersParser, IWebBrowser webBrowser, IAnalyticsController analytics, CancellationToken ct) + private async UniTask> VerifyMinimumHardwareRequirementMetAsync(IAppArgs applicationParametersParser, UnityAppWebBrowser webBrowser, IAnalyticsController analytics, CancellationToken ct) { var minimumSpecsGuard = new MinimumSpecsGuard(new DefaultSpecProfileProvider(), new UnitySystemInfoProvider()); diff --git a/Explorer/Assets/DCL/Infrastructure/MVC/MVCFacade/MVCManagerMenusAccessFacade.cs b/Explorer/Assets/DCL/Infrastructure/MVC/MVCFacade/MVCManagerMenusAccessFacade.cs index 195d3e09ddf..78c51f545df 100644 --- a/Explorer/Assets/DCL/Infrastructure/MVC/MVCFacade/MVCManagerMenusAccessFacade.cs +++ b/Explorer/Assets/DCL/Infrastructure/MVC/MVCFacade/MVCManagerMenusAccessFacade.cs @@ -46,7 +46,7 @@ public class MVCManagerMenusAccessFacade : IMVCManagerMenusAccessFacade private readonly IVoiceChatOrchestrator voiceChatOrchestrator; private readonly bool includeCommunities; private readonly CommunitiesDataProvider communitiesDataProvider; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly ISelfProfile selfProfile; private readonly NearbyMuteService? nearbyMuteService; @@ -71,7 +71,7 @@ public MVCManagerMenusAccessFacade( IVoiceChatOrchestrator voiceChatOrchestrator, bool includeCommunities, CommunitiesDataProvider communitiesDataProvider, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource, ISelfProfile selfProfile, NearbyMuteService? nearbyMuteService = null) diff --git a/Explorer/Assets/DCL/MarketplaceCredits/MarketplaceCreditsMenuController.cs b/Explorer/Assets/DCL/MarketplaceCredits/MarketplaceCreditsMenuController.cs index e5d00d1d128..c4f6065ccc0 100644 --- a/Explorer/Assets/DCL/MarketplaceCredits/MarketplaceCreditsMenuController.cs +++ b/Explorer/Assets/DCL/MarketplaceCredits/MarketplaceCreditsMenuController.cs @@ -46,7 +46,7 @@ public partial class MarketplaceCreditsMenuController : ControllerBase viewInstance?.InfoLinkButtonTooltip.Show(); private void OpenInfoLink() => - webBrowser.OpenUrl(WEEKLY_REWARDS_INFO_LINK); + webBrowser.OpenUrlMainThreadOnly(WEEKLY_REWARDS_INFO_LINK); private void OpenGoShoppingLink() => - webBrowser.OpenUrl(DecentralandUrl.GoShoppingWithMarketplaceCredits); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.GoShoppingWithMarketplaceCredits); private async UniTaskVoid ShowErrorNotificationAsync(string message, CancellationToken ct) { diff --git a/Explorer/Assets/DCL/MarketplaceCredits/Sections/MarketplaceCreditsProgramEndedSubController.cs b/Explorer/Assets/DCL/MarketplaceCredits/Sections/MarketplaceCreditsProgramEndedSubController.cs index f0c806850a2..1b5627f98c8 100644 --- a/Explorer/Assets/DCL/MarketplaceCredits/Sections/MarketplaceCreditsProgramEndedSubController.cs +++ b/Explorer/Assets/DCL/MarketplaceCredits/Sections/MarketplaceCreditsProgramEndedSubController.cs @@ -29,11 +29,11 @@ public class MarketplaceCreditsProgramEndedSubController : IDisposable private readonly string subtitleMarketOffline = $"Please check the #product-status channel in Decentraland's Discord server for updates if service does not resume shortly."; private readonly MarketplaceCreditsProgramEndedSubView subView; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; public MarketplaceCreditsProgramEndedSubController( MarketplaceCreditsProgramEndedSubView subView, - IWebBrowser webBrowser) + UnityAppWebBrowser webBrowser) { this.subView = subView; this.webBrowser = webBrowser; @@ -134,13 +134,13 @@ private void OpenLink(string id) switch (id) { case SUBSCRIBE_LINK_ID: - webBrowser.OpenUrl(DecentralandUrl.NewsletterSubscriptionLink); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.NewsletterSubscriptionLink); break; case X_LINK_ID: - webBrowser.OpenUrl(DecentralandUrl.TwitterLink); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.TwitterLink); break; case DISCORD_LINK_ID: - webBrowser.OpenUrl(DecentralandUrl.DiscordDirectLink); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.DiscordDirectLink); break; } diff --git a/Explorer/Assets/DCL/MarketplaceCredits/Sections/MarketplaceCreditsWelcomeSubController.cs b/Explorer/Assets/DCL/MarketplaceCredits/Sections/MarketplaceCreditsWelcomeSubController.cs index 3c3cebae140..40a5d3039fa 100644 --- a/Explorer/Assets/DCL/MarketplaceCredits/Sections/MarketplaceCreditsWelcomeSubController.cs +++ b/Explorer/Assets/DCL/MarketplaceCredits/Sections/MarketplaceCreditsWelcomeSubController.cs @@ -21,7 +21,7 @@ public class MarketplaceCreditsWelcomeSubController : IDisposable private readonly MarketplaceCreditsGoalsOfTheWeekSubController marketplaceCreditsGoalsOfTheWeekSubController; private readonly MarketplaceCreditsWeekGoalsCompletedSubController marketplaceCreditsWeekGoalsCompletedSubController; private readonly MarketplaceCreditsProgramEndedSubController marketplaceCreditsProgramEndedSubController; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly MarketplaceCreditsAPIClient marketplaceCreditsAPIClient; private readonly ISelfProfile selfProfile; private readonly IInputBlock inputBlock; @@ -38,7 +38,7 @@ public MarketplaceCreditsWelcomeSubController( MarketplaceCreditsGoalsOfTheWeekSubController marketplaceCreditsGoalsOfTheWeekSubController, MarketplaceCreditsWeekGoalsCompletedSubController marketplaceCreditsWeekGoalsCompletedSubController, MarketplaceCreditsProgramEndedSubController marketplaceCreditsProgramEndedSubController, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, MarketplaceCreditsAPIClient marketplaceCreditsAPIClient, ISelfProfile selfProfile, IInputBlock inputBlock) @@ -243,7 +243,7 @@ private void RedirectToSection(bool ignoreHasUserStartedProgramFlag = false) } private void OpenLearnMoreLink() => - webBrowser.OpenUrl(MarketplaceCreditsMenuController.WEEKLY_REWARDS_INFO_LINK); + webBrowser.OpenUrlMainThreadOnly(MarketplaceCreditsMenuController.WEEKLY_REWARDS_INFO_LINK); } diff --git a/Explorer/Assets/DCL/Navmap/EventInfoPanelController.cs b/Explorer/Assets/DCL/Navmap/EventInfoPanelController.cs index a412305dd34..4b03746a0ad 100644 --- a/Explorer/Assets/DCL/Navmap/EventInfoPanelController.cs +++ b/Explorer/Assets/DCL/Navmap/EventInfoPanelController.cs @@ -26,7 +26,7 @@ public class EventInfoPanelController private readonly ObjectPool scheduleElementPool; private readonly GoogleUserCalendar userCalendar; private readonly SharePlacesAndEventsContextMenuController shareContextMenu; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly ImageController thumbnailController; private readonly MultiStateButtonController interestedButtonController; @@ -43,7 +43,7 @@ public EventInfoPanelController(EventInfoPanelView view, ObjectPool scheduleElementPool, GoogleUserCalendar userCalendar, SharePlacesAndEventsContextMenuController shareContextMenu, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource, ImageControllerProvider imageControllerProvider) { @@ -128,7 +128,7 @@ public void Set(EventDTO @event, PlacesData.PlaceInfo place) } private void OpenUrl(string url) => - webBrowser.OpenUrl(url); + webBrowser.OpenUrlMainThreadOnly(url); private void AddRecurrentEvents(EventDTO @event) { diff --git a/Explorer/Assets/DCL/Navmap/PlaceInfoPanelController.cs b/Explorer/Assets/DCL/Navmap/PlaceInfoPanelController.cs index 7f1096dec29..7e2c2d1ad56 100644 --- a/Explorer/Assets/DCL/Navmap/PlaceInfoPanelController.cs +++ b/Explorer/Assets/DCL/Navmap/PlaceInfoPanelController.cs @@ -41,7 +41,7 @@ public class PlaceInfoPanelController : IDisposable private readonly HttpEventsApiService eventsApiService; private readonly ObjectPool eventElementPool ; private readonly SharePlacesAndEventsContextMenuController shareContextMenu; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IMVCManager mvcManager; private readonly GalleryEventBus galleryEventBus; private readonly HomePlaceEventBus homePlaceEventBus; @@ -73,7 +73,7 @@ public PlaceInfoPanelController(PlaceInfoPanelView view, HttpEventsApiService eventsApiService, ObjectPool eventElementPool, SharePlacesAndEventsContextMenuController shareContextMenu, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IMVCManager mvcManager, HomePlaceEventBus homePlaceEventBus, IDonationsService donationsService, @@ -373,7 +373,7 @@ private void Share() } private void OpenUrl(string url) => - webBrowser.OpenUrl(url); + webBrowser.OpenUrlMainThreadOnly(url); private void OnLikeButtonClick(bool isEnabled) { diff --git a/Explorer/Assets/DCL/Navmap/SatelliteController.cs b/Explorer/Assets/DCL/Navmap/SatelliteController.cs index 9e0eb7f299c..8dde0f601b0 100644 --- a/Explorer/Assets/DCL/Navmap/SatelliteController.cs +++ b/Explorer/Assets/DCL/Navmap/SatelliteController.cs @@ -1,75 +1,75 @@ -using DCL.Browser; -using DCL.Character.CharacterMotion.Components; -using DCL.MapRenderer; -using DCL.MapRenderer.ConsumerUtils; -using DCL.MapRenderer.MapCameraController; -using DCL.MapRenderer.MapLayers; -using DCL.UI; -using System; -using UnityEngine; - -namespace DCL.Navmap -{ - public class SatelliteController : ISection - { - private const string GENESIS_CITY_LINK = "https://genesis.city/"; - - private readonly SatelliteView view; - private readonly RectTransform rectTransform; - private readonly IMapRenderer mapRenderer; - private readonly IWebBrowser webBrowser; - - private IMapCameraController cameraController; - - public SatelliteController( - SatelliteView view, - MapCameraDragBehavior.MapCameraDragBehaviorData mapCameraDragBehaviorData, - IMapRenderer mapRenderer, - IWebBrowser webBrowser) - { - this.view = view; - this.mapRenderer = mapRenderer; - this.webBrowser = webBrowser; - - rectTransform = view.GetComponent(); - view.SatelliteRenderImage.EmbedMapCameraDragBehavior(mapCameraDragBehaviorData); - view.OnClickedGenesisCityLink += OnClickedGenesisCityLink; - } - - private void OnClickedGenesisCityLink() - { - webBrowser.OpenUrl(GENESIS_CITY_LINK); - } - - public void InjectCameraController(IMapCameraController controller) - { - cameraController = controller; - } - - public void Activate() - { - view.SatelliteRenderImage.texture = cameraController.GetRenderTexture(); - view.SatellitePixelPerfectMapRendererTextureProvider.Activate(cameraController); - view.SatelliteRenderImage.Activate(null, cameraController.GetRenderTexture(), cameraController); - view.gameObject.SetActive(true); - } - - public void Deactivate() - { - mapRenderer.SetSharedLayer(MapLayer.SatelliteAtlas, false); - view.SatellitePixelPerfectMapRendererTextureProvider.Deactivate(); - view.SatelliteRenderImage.Deactivate(); - view.gameObject.SetActive(false); - } - - public void Animate(int triggerId) - { - view.gameObject.SetActive(triggerId == UIAnimationHashes.IN); - } - - public void ResetAnimator() { } - - public RectTransform GetRectTransform() => - rectTransform; - } -} +using DCL.Browser; +using DCL.Character.CharacterMotion.Components; +using DCL.MapRenderer; +using DCL.MapRenderer.ConsumerUtils; +using DCL.MapRenderer.MapCameraController; +using DCL.MapRenderer.MapLayers; +using DCL.UI; +using System; +using UnityEngine; + +namespace DCL.Navmap +{ + public class SatelliteController : ISection + { + private const string GENESIS_CITY_LINK = "https://genesis.city/"; + + private readonly SatelliteView view; + private readonly RectTransform rectTransform; + private readonly IMapRenderer mapRenderer; + private readonly UnityAppWebBrowser webBrowser; + + private IMapCameraController cameraController; + + public SatelliteController( + SatelliteView view, + MapCameraDragBehavior.MapCameraDragBehaviorData mapCameraDragBehaviorData, + IMapRenderer mapRenderer, + UnityAppWebBrowser webBrowser) + { + this.view = view; + this.mapRenderer = mapRenderer; + this.webBrowser = webBrowser; + + rectTransform = view.GetComponent(); + view.SatelliteRenderImage.EmbedMapCameraDragBehavior(mapCameraDragBehaviorData); + view.OnClickedGenesisCityLink += OnClickedGenesisCityLink; + } + + private void OnClickedGenesisCityLink() + { + webBrowser.OpenUrlMainThreadOnly(GENESIS_CITY_LINK); + } + + public void InjectCameraController(IMapCameraController controller) + { + cameraController = controller; + } + + public void Activate() + { + view.SatelliteRenderImage.texture = cameraController.GetRenderTexture(); + view.SatellitePixelPerfectMapRendererTextureProvider.Activate(cameraController); + view.SatelliteRenderImage.Activate(null, cameraController.GetRenderTexture(), cameraController); + view.gameObject.SetActive(true); + } + + public void Deactivate() + { + mapRenderer.SetSharedLayer(MapLayer.SatelliteAtlas, false); + view.SatellitePixelPerfectMapRendererTextureProvider.Deactivate(); + view.SatelliteRenderImage.Deactivate(); + view.gameObject.SetActive(false); + } + + public void Animate(int triggerId) + { + view.gameObject.SetActive(triggerId == UIAnimationHashes.IN); + } + + public void ResetAnimator() { } + + public RectTransform GetRectTransform() => + rectTransform; + } +} diff --git a/Explorer/Assets/DCL/Navmap/SharePlacesAndEventsContextMenuController.cs b/Explorer/Assets/DCL/Navmap/SharePlacesAndEventsContextMenuController.cs index 021ddefe502..ab2df08b59e 100644 --- a/Explorer/Assets/DCL/Navmap/SharePlacesAndEventsContextMenuController.cs +++ b/Explorer/Assets/DCL/Navmap/SharePlacesAndEventsContextMenuController.cs @@ -19,7 +19,7 @@ public class SharePlacesAndEventsContextMenuController private readonly SharePlacesAndEventsContextMenuView view; private readonly WarningNotificationView warningNotificationView; private readonly ISystemClipboard clipboard; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IDecentralandUrlsSource decentralandUrlsSource; private string? twitterLink; private string? copyLink; @@ -28,7 +28,7 @@ public class SharePlacesAndEventsContextMenuController public SharePlacesAndEventsContextMenuController(SharePlacesAndEventsContextMenuView view, WarningNotificationView warningNotificationView, ISystemClipboard clipboard, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource) { this.view = view; @@ -94,7 +94,7 @@ async UniTaskVoid ShowToastAsync(CancellationToken ct) private void ShareOnTwitter() { if (string.IsNullOrEmpty(twitterLink)) return; - webBrowser.OpenUrl(twitterLink); + webBrowser.OpenUrlMainThreadOnly(twitterLink); Hide(); } } diff --git a/Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs b/Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs deleted file mode 100644 index 802151a3118..00000000000 --- a/Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs +++ /dev/null @@ -1,13 +0,0 @@ -using DCL.Multiplayer.Connections.DecentralandUrls; - -namespace DCL.Browser -{ - public interface IWebBrowser - { - void OpenUrl(string url); - - void OpenUrl(DecentralandUrl url); - - string GetUrl(DecentralandUrl url); - } -} diff --git a/Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs.meta b/Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs.meta deleted file mode 100644 index ab227da5574..00000000000 --- a/Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 6dfb94a703104a5ba0989671bd46c74e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Explorer/Assets/DCL/NetworkDefinitions/Browser/SupportRequestService.cs b/Explorer/Assets/DCL/NetworkDefinitions/Browser/SupportRequestService.cs index 2798017a40a..e746347dc37 100644 --- a/Explorer/Assets/DCL/NetworkDefinitions/Browser/SupportRequestService.cs +++ b/Explorer/Assets/DCL/NetworkDefinitions/Browser/SupportRequestService.cs @@ -5,18 +5,18 @@ namespace DCL.Browser { public class SupportRequestService { - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; public event Action? SupportRequested; - public SupportRequestService(IWebBrowser webBrowser) + public SupportRequestService(UnityAppWebBrowser webBrowser) { this.webBrowser = webBrowser; } public void OpenSupport() { - webBrowser.OpenUrl(DecentralandUrl.Help); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.Help); SupportRequested?.Invoke(); } } diff --git a/Explorer/Assets/DCL/NetworkDefinitions/Browser/UnityAppWebBrowser.cs b/Explorer/Assets/DCL/NetworkDefinitions/Browser/UnityAppWebBrowser.cs index 32928a00c52..8ac28e6ebc0 100644 --- a/Explorer/Assets/DCL/NetworkDefinitions/Browser/UnityAppWebBrowser.cs +++ b/Explorer/Assets/DCL/NetworkDefinitions/Browser/UnityAppWebBrowser.cs @@ -4,7 +4,7 @@ namespace DCL.Browser { - public class UnityAppWebBrowser : IWebBrowser + public class UnityAppWebBrowser { private readonly IDecentralandUrlsSource decentralandUrlsSource; @@ -13,14 +13,14 @@ public UnityAppWebBrowser(IDecentralandUrlsSource decentralandUrlsSource) this.decentralandUrlsSource = decentralandUrlsSource; } - public void OpenUrl(string url) + public void OpenUrlMainThreadOnly(string url) { Application.OpenURL(Uri.EscapeUriString(url)); } - public void OpenUrl(DecentralandUrl url) + public void OpenUrlMainThreadOnly(DecentralandUrl url) { - OpenUrl(decentralandUrlsSource.Url(url)); + OpenUrlMainThreadOnly(decentralandUrlsSource.Url(url)); } public string GetUrl(DecentralandUrl url) => diff --git a/Explorer/Assets/DCL/NftPrompt/NftPromptController.cs b/Explorer/Assets/DCL/NftPrompt/NftPromptController.cs index 70a11ad7d7e..c5896016b61 100644 --- a/Explorer/Assets/DCL/NftPrompt/NftPromptController.cs +++ b/Explorer/Assets/DCL/NftPrompt/NftPromptController.cs @@ -21,7 +21,7 @@ public partial class NftPromptController : ControllerBase CanvasOrdering.SortingLayer.POPUP; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly ICursor cursor; private readonly INftMarketAPIClient nftInfoAPIClient; private readonly ImageControllerProvider imageControllerProvider; @@ -34,7 +34,7 @@ public partial class NftPromptController : ControllerBase loadingItemsPool; @@ -57,7 +57,7 @@ public EquippedItems_PassportModuleController( NFTColorsSO rarityColors, NftTypeIconSO categoryIcons, IThumbnailProvider thumbnailProvider, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource, PassportErrorsController passportErrorsController) { @@ -197,7 +197,7 @@ private void SetGridElements(List gridWearables, IReadOnlyList webBrowser.OpenUrl(marketPlaceLink)); + equippedWearableItem.BuyButton.onClick.AddListener(() => webBrowser.OpenUrlMainThreadOnly(marketPlaceLink)); WaitForThumbnailAsync(wearable, equippedWearableItem, getEquippedItemsCts.Token).Forget(); instantiatedEquippedItems.Add(equippedWearableItem); elementsAddedInTheGird++; @@ -220,7 +220,7 @@ private void SetGridElements(List gridWearables, IReadOnlyList webBrowser.OpenUrl(marketPlaceLink)); + equippedWearableItem.BuyButton.onClick.AddListener(() => webBrowser.OpenUrlMainThreadOnly(marketPlaceLink)); WaitForThumbnailAsync(emote, equippedWearableItem, getEquippedItemsCts.Token).Forget(); instantiatedEquippedItems.Add(equippedWearableItem); elementsAddedInTheGird++; diff --git a/Explorer/Assets/DCL/Passport/Modules/UserBasicInfo_PassportModuleController.cs b/Explorer/Assets/DCL/Passport/Modules/UserBasicInfo_PassportModuleController.cs index 0854a2fcc94..5148ad252cd 100644 --- a/Explorer/Assets/DCL/Passport/Modules/UserBasicInfo_PassportModuleController.cs +++ b/Explorer/Assets/DCL/Passport/Modules/UserBasicInfo_PassportModuleController.cs @@ -22,7 +22,7 @@ public class UserBasicInfo_PassportModuleController : IPassportModuleController private readonly UserWalletAddressElementPresenter walletAddressElementPresenter; private readonly UserBasicInfo_PassportModuleView view; private readonly ISelfProfile selfProfile; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IMVCManager mvcManager; private readonly INftNamesProvider nftNamesProvider; private readonly IDecentralandUrlsSource decentralandUrlsSource; @@ -38,7 +38,7 @@ public class UserBasicInfo_PassportModuleController : IPassportModuleController public UserBasicInfo_PassportModuleController( UserBasicInfo_PassportModuleView view, ISelfProfile selfProfile, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IMVCManager mvcManager, INftNamesProvider nftNamesProvider, IDecentralandUrlsSource decentralandUrlsSource, @@ -148,7 +148,7 @@ async UniTaskVoid ShowNameEditorAsync(CancellationToken ct) private void ClaimName() { - webBrowser.OpenUrl(decentralandUrlsSource.Url(DecentralandUrl.MarketplaceClaimName)); + webBrowser.OpenUrlMainThreadOnly(decentralandUrlsSource.Url(DecentralandUrl.MarketplaceClaimName)); NameClaimRequested?.Invoke(); } } diff --git a/Explorer/Assets/DCL/Passport/PassportController.cs b/Explorer/Assets/DCL/Passport/PassportController.cs index cc87df23e83..2671f301e96 100644 --- a/Explorer/Assets/DCL/Passport/PassportController.cs +++ b/Explorer/Assets/DCL/Passport/PassportController.cs @@ -85,7 +85,7 @@ private enum OpenBadgeSectionOrigin private readonly ISelfProfile selfProfile; private readonly World world; private readonly IThumbnailProvider thumbnailProvider; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly BadgesAPIClient badgesAPIClient; private readonly PassportProfileInfoController passportProfileInfoController; @@ -176,7 +176,7 @@ public PassportController( World world, Entity playerEntity, IThumbnailProvider thumbnailProvider, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource, BadgesAPIClient badgesAPIClient, IInputBlock inputBlock, diff --git a/Explorer/Assets/DCL/Places/PlacesCardSocialActionsController.cs b/Explorer/Assets/DCL/Places/PlacesCardSocialActionsController.cs index 74b72ff9570..626a4025b0a 100644 --- a/Explorer/Assets/DCL/Places/PlacesCardSocialActionsController.cs +++ b/Explorer/Assets/DCL/Places/PlacesCardSocialActionsController.cs @@ -39,7 +39,7 @@ public class PlacesCardSocialActionsController private readonly IPlacesAPIService placesAPIService; private readonly IRealmNavigator realmNavigator; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly ISystemClipboard clipboard; private readonly IDecentralandUrlsSource dclUrlSource; private readonly INavmapBus? navmapBus; @@ -49,7 +49,7 @@ public class PlacesCardSocialActionsController public PlacesCardSocialActionsController( IPlacesAPIService placesAPIService, IRealmNavigator realmNavigator, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, ISystemClipboard clipboard, IDecentralandUrlsSource dclUrlSource, INavmapBus? navmapBus, @@ -194,7 +194,7 @@ public void SharePlace(PlacesData.PlaceInfo placeInfo) var description = string.Format(TWITTER_PLACE_DESCRIPTION, placeInfo.title); var twitterLink = string.Format(dclUrlSource.Url(DecentralandUrl.TwitterNewPostLink), description, "DCLPlace", GetPlaceCopyLink(placeInfo)); - webBrowser.OpenUrl(twitterLink); + webBrowser.OpenUrlMainThreadOnly(twitterLink); PlaceShared?.Invoke(placeInfo); } diff --git a/Explorer/Assets/DCL/Places/PlacesController.cs b/Explorer/Assets/DCL/Places/PlacesController.cs index 1c4515c7bd9..f8ae990f45c 100644 --- a/Explorer/Assets/DCL/Places/PlacesController.cs +++ b/Explorer/Assets/DCL/Places/PlacesController.cs @@ -41,7 +41,7 @@ public PlacesController( PlaceCategoriesSO placesCategories, IInputBlock inputBlock, ISelfProfile selfProfile, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IFriendsService? friendsService, ProfileRepositoryWrapper profileRepositoryWrapper, IMVCManager mvcManager, diff --git a/Explorer/Assets/DCL/Places/PlacesResultsController.cs b/Explorer/Assets/DCL/Places/PlacesResultsController.cs index 036590be0bb..d1a403ebbfb 100644 --- a/Explorer/Assets/DCL/Places/PlacesResultsController.cs +++ b/Explorer/Assets/DCL/Places/PlacesResultsController.cs @@ -45,7 +45,7 @@ public class PlacesResultsController : IDisposable private readonly IPlacesAPIService placesAPIService; private readonly PlacesStateService placesStateService; private readonly ISelfProfile selfProfile; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly PlacesCardSocialActionsController placesCardSocialActionsController; private readonly IFriendsService? friendsService; private readonly IMVCManager mvcManager; @@ -69,7 +69,7 @@ public PlacesResultsController( IPlacesAPIService placesAPIService, PlacesStateService placesStateService, ISelfProfile selfProfile, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IFriendsService? friendsService, ProfileRepositoryWrapper profileRepositoryWrapper, IMVCManager mvcManager, @@ -140,7 +140,7 @@ private void OnExplorePlacesClicked() => placesController.OpenSection(PlacesSection.BROWSE, force: true, resetCategory: true); private void GetANameClicked() => - webBrowser.OpenUrl(DecentralandUrl.MarketplaceClaimName); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.MarketplaceClaimName); private void TryLoadMorePlaces() { diff --git a/Explorer/Assets/DCL/PluginSystem/Global/BackpackSubPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/BackpackSubPlugin.cs index 8b5164019e8..1579136ba1a 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/BackpackSubPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/BackpackSubPlugin.cs @@ -58,7 +58,7 @@ internal class BackpackSubPlugin : IDisposable private readonly CharacterPreviewEventBus characterPreviewEventBus; private readonly IInputBlock inputBlock; private readonly IAppArgs appArgs; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly WarningNotificationView inWorldWarningNotificationView; private readonly IThumbnailProvider thumbnailProvider; private readonly ProfileChangesBus profileChangesBus; @@ -97,7 +97,7 @@ public BackpackSubPlugin( Arch.Core.World world, Entity playerEntity, IAppArgs appArgs, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, WarningNotificationView inWorldWarningNotificationView, IThumbnailProvider thumbnailProvider, ProfileChangesBus profileChangesBus, diff --git a/Explorer/Assets/DCL/PluginSystem/Global/CommunitiesPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/CommunitiesPlugin.cs index ca2b644c4e7..ae35e7289c9 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/CommunitiesPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/CommunitiesPlugin.cs @@ -54,7 +54,7 @@ public class CommunitiesPlugin : IDCLGlobalPlugin private readonly ISelfProfile selfProfile; private readonly IRealmNavigator realmNavigator; private readonly ISystemClipboard clipboard; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly HttpEventsApiService eventsApiService; private readonly ChatEventBus chatEventBus; private readonly RPCCommunitiesService rpcCommunitiesService; @@ -86,7 +86,7 @@ public CommunitiesPlugin(IMVCManager mvcManager, ISelfProfile selfProfile, IRealmNavigator realmNavigator, ISystemClipboard clipboard, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, HttpEventsApiService eventsApiService, ChatEventBus chatEventBus, GalleryEventBus galleryEventBus, diff --git a/Explorer/Assets/DCL/PluginSystem/Global/DonationsPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/DonationsPlugin.cs index 305e0dfb77e..eef878030d3 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/DonationsPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/DonationsPlugin.cs @@ -33,7 +33,7 @@ public class DonationsPlugin : IDCLGlobalPlugin private readonly IProfileRepository profileRepository; private readonly Entity playerEntity; private readonly Arch.Core.World world; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly IInputBlock inputBlock; private readonly ICompositeWeb3Provider web3Provider; @@ -46,7 +46,7 @@ public DonationsPlugin(IMVCManager mvcManager, IProfileRepository profileRepository, Entity playerEntity, Arch.Core.World world, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource, IInputBlock inputBlock, ICompositeWeb3Provider web3Provider) diff --git a/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs index f2e11b1a17c..cb642ae91e4 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs @@ -111,7 +111,7 @@ public class ExplorePanelPlugin : IDCLGlobalPlugin { private readonly IAssetsProvisioner assetsProvisioner; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IMVCManager mvcManager; private readonly ICursor cursor; private ExternalUrlPromptController? externalUrlPromptController; public ExternalUrlPromptPlugin( IAssetsProvisioner assetsProvisioner, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IMVCManager mvcManager, ICursor cursor) { diff --git a/Explorer/Assets/DCL/PluginSystem/Global/FriendsContainer.cs b/Explorer/Assets/DCL/PluginSystem/Global/FriendsContainer.cs index 57624d7d943..7cee3da599d 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/FriendsContainer.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/FriendsContainer.cs @@ -89,7 +89,7 @@ public FriendsContainer( IUserBlockingCache injectedUserBlockingCache, ProfileRepositoryWrapper profileDataProvider, IVoiceChatOrchestrator voiceChatOrchestrator, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource) { this.mainUIView = mainUIView; diff --git a/Explorer/Assets/DCL/PluginSystem/Global/GiftingPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/GiftingPlugin.cs index b387a110963..b7e66f01d46 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/GiftingPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/GiftingPlugin.cs @@ -52,7 +52,7 @@ public class GiftingPlugin : IDCLGlobalPlugin private readonly IWeb3IdentityCache web3IdentityCache; private readonly IThumbnailProvider thumbnailProvider; private readonly IEventBus eventBus; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly ICompositeWeb3Provider web3Provider; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly ImageControllerProvider imageControllerProvider; @@ -76,7 +76,7 @@ public GiftingPlugin(IAssetsProvisioner assetsProvisioner, IWeb3IdentityCache web3IdentityCache, IThumbnailProvider thumbnailProvider, IEventBus eventBus, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, ICompositeWeb3Provider web3Provider, IDecentralandUrlsSource decentralandUrlsSource, ImageControllerProvider imageControllerProvider) diff --git a/Explorer/Assets/DCL/PluginSystem/Global/MarketplaceCreditsPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/MarketplaceCreditsPlugin.cs index 09daff28fb8..02006bcfd2d 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/MarketplaceCreditsPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/MarketplaceCreditsPlugin.cs @@ -25,7 +25,7 @@ public class MarketplaceCreditsPlugin : IDCLGlobalPlugin { private readonly IAssetsProvisioner assetsProvisioner; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IMVCManager mvcManager; private readonly INftMarketAPIClient nftInfoAPIClient; private readonly ImageControllerProvider imageControllerProvider; @@ -26,7 +26,7 @@ public class NftPromptPlugin : IDCLGlobalPlugin private readonly ICharacterPreviewFactory characterPreviewFactory; private readonly CharacterPreviewEventBus characterPreviewEventBus; private readonly ISelfProfile selfProfile; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly BadgesAPIClient badgesAPIClient; private readonly IInputBlock inputBlock; @@ -79,7 +79,7 @@ public PassportPlugin( ICharacterPreviewFactory characterPreviewFactory, CharacterPreviewEventBus characterPreviewEventBus, ISelfProfile selfProfile, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource, BadgesAPIClient badgesAPIClient, IInputBlock inputBlock, diff --git a/Explorer/Assets/DCL/PluginSystem/Global/SidebarPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/SidebarPlugin.cs index 919fe21b651..5cbfbbe9b76 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/SidebarPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/SidebarPlugin.cs @@ -50,7 +50,7 @@ public class SidebarPlugin : IDCLGlobalPlugin private readonly IWeb3IdentityCache web3IdentityCache; private readonly IProfileRepository profileRepository; private readonly IWebRequestController webRequestController; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IWeb3Authenticator web3Authenticator; private readonly IUserInAppInitializationFlow userInAppInitializationFlow; private readonly IProfileCache profileCache; @@ -91,7 +91,7 @@ public SidebarPlugin( IWeb3IdentityCache web3IdentityCache, IProfileRepository profileRepository, IWebRequestController webRequestController, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IWeb3Authenticator web3Authenticator, IUserInAppInitializationFlow userInAppInitializationFlow, IProfileCache profileCache, diff --git a/Explorer/Assets/DCL/PluginSystem/Global/Web3AuthenticationPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/Web3AuthenticationPlugin.cs index 17aadf4a6e9..e30f96da162 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/Web3AuthenticationPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/Web3AuthenticationPlugin.cs @@ -32,7 +32,7 @@ public class Web3AuthenticationPlugin : IDCLGlobalPlugin private readonly IDebugContainerBuilder debugContainerBuilder; private readonly IMVCManager mvcManager; private readonly ISelfProfile selfProfile; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IRealmData realmData; private readonly IWeb3IdentityCache storedIdentityProvider; private readonly ICharacterPreviewFactory characterPreviewFactory; @@ -58,7 +58,7 @@ public Web3AuthenticationPlugin( IDebugContainerBuilder debugContainerBuilder, IMVCManager mvcManager, ISelfProfile selfProfile, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IRealmData realmData, IWeb3IdentityCache storedIdentityProvider, ICharacterPreviewFactory characterPreviewFactory, diff --git a/Explorer/Assets/DCL/UI/ConfirmationDialog/ReportUserHelper.cs b/Explorer/Assets/DCL/UI/ConfirmationDialog/ReportUserHelper.cs index 1f11596bc50..12abcb330db 100644 --- a/Explorer/Assets/DCL/UI/ConfirmationDialog/ReportUserHelper.cs +++ b/Explorer/Assets/DCL/UI/ConfirmationDialog/ReportUserHelper.cs @@ -19,7 +19,7 @@ public static async UniTask ShowConfirmAndReportAsync( string reportCategory, string reportedUserId, ISelfProfile selfProfile, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource, CancellationToken ct) { @@ -36,7 +36,7 @@ public static async UniTask ShowConfirmAndReportAsync( Profile? ownProfile = await selfProfile.ProfileAsync(ct); - webBrowser.OpenUrl(string.Format(decentralandUrlsSource.Url(DecentralandUrl.ReportUserForm), + webBrowser.OpenUrlMainThreadOnly(string.Format(decentralandUrlsSource.Url(DecentralandUrl.ReportUserForm), ownProfile != null ? ownProfile.UserId : string.Empty, reportedUserId)); } diff --git a/Explorer/Assets/DCL/UI/GenericContextMenu/Controllers/GenericUserProfileContextMenuController.cs b/Explorer/Assets/DCL/UI/GenericContextMenu/Controllers/GenericUserProfileContextMenuController.cs index f4e5ead2c26..9aa9f0f4d30 100644 --- a/Explorer/Assets/DCL/UI/GenericContextMenu/Controllers/GenericUserProfileContextMenuController.cs +++ b/Explorer/Assets/DCL/UI/GenericContextMenu/Controllers/GenericUserProfileContextMenuController.cs @@ -73,7 +73,7 @@ public class GenericUserProfileContextMenuController private readonly bool isCommunitiesFeatureEnabled; private readonly bool isUserBlockingFeatureEnabled; private readonly IVoiceChatOrchestratorActions voiceChatOrchestrator; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IDecentralandUrlsSource decentralandUrlsSource; private readonly GenericUserProfileContextMenuSettings contextMenuSettings; private readonly ISelfProfile selfProfile; @@ -121,7 +121,7 @@ public GenericUserProfileContextMenuController( bool isCommunitiesFeatureEnabled, CommunitiesDataProvider communitiesDataProvider, IVoiceChatOrchestratorActions voiceChatOrchestrator, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IDecentralandUrlsSource decentralandUrlsSource, ISelfProfile selfProfile, NearbyMuteService? nearbyMuteService = null) diff --git a/Explorer/Assets/DCL/UI/Profiles/Names/ProfileNameEditorController.cs b/Explorer/Assets/DCL/UI/Profiles/Names/ProfileNameEditorController.cs index 2184dad8f76..cd87bc327da 100644 --- a/Explorer/Assets/DCL/UI/Profiles/Names/ProfileNameEditorController.cs +++ b/Explorer/Assets/DCL/UI/Profiles/Names/ProfileNameEditorController.cs @@ -16,7 +16,7 @@ namespace DCL.UI.ProfileNames { public class ProfileNameEditorController : ControllerBase { - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly ISelfProfile selfProfile; private readonly INftNamesProvider nftNamesProvider; private readonly IDecentralandUrlsSource decentralandUrlsSource; @@ -32,7 +32,7 @@ public class ProfileNameEditorController : ControllerBase public event Action? NameClaimRequested; public ProfileNameEditorController(ViewFactoryMethod viewFactory, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, ISelfProfile selfProfile, INftNamesProvider nftNamesProvider, IDecentralandUrlsSource decentralandUrlsSource, @@ -85,7 +85,7 @@ protected override void OnViewInstantiated() claimedConfig.saveButtonInteractable = i != -1; }); - claimedConfig.clickeableLink.OnLinkClicked += url => webBrowser.OpenUrl(url); + claimedConfig.clickeableLink.OnLinkClicked += url => webBrowser.OpenUrlMainThreadOnly(url); viewInstance.OverlayCloseButton.onClick.AddListener(Close); @@ -180,7 +180,7 @@ void SetUpNonClaimed(ProfileNameEditorView.NonClaimedNameConfig config, Profile private void ClaimNewName() { - webBrowser.OpenUrl(decentralandUrlsSource.Url(DecentralandUrl.MarketplaceClaimName)); + webBrowser.OpenUrlMainThreadOnly(decentralandUrlsSource.Url(DecentralandUrl.MarketplaceClaimName)); NameClaimRequested?.Invoke(); } diff --git a/Explorer/Assets/DCL/UI/Profiles/ProfileMenuController.cs b/Explorer/Assets/DCL/UI/Profiles/ProfileMenuController.cs index 71435f954ae..efa6c083291 100644 --- a/Explorer/Assets/DCL/UI/Profiles/ProfileMenuController.cs +++ b/Explorer/Assets/DCL/UI/Profiles/ProfileMenuController.cs @@ -20,7 +20,7 @@ public class ProfileMenuController : ControllerBase private readonly IWeb3IdentityCache identityCache; private readonly World world; private readonly Entity playerEntity; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IWeb3Authenticator web3Authenticator; private readonly IUserInAppInitializationFlow userInAppInitializationFlow; private readonly IProfileCache profileCache; @@ -36,7 +36,7 @@ public ProfileMenuController( IWeb3IdentityCache identityCache, World world, Entity playerEntity, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IWeb3Authenticator web3Authenticator, IUserInAppInitializationFlow userInAppInitializationFlow, IProfileCache profileCache, diff --git a/Explorer/Assets/DCL/UI/Sidebar/HelpMenu/HelpMenuController.cs b/Explorer/Assets/DCL/UI/Sidebar/HelpMenu/HelpMenuController.cs index e59f442fcfe..4c4637f7fd4 100644 --- a/Explorer/Assets/DCL/UI/Sidebar/HelpMenu/HelpMenuController.cs +++ b/Explorer/Assets/DCL/UI/Sidebar/HelpMenu/HelpMenuController.cs @@ -11,7 +11,7 @@ namespace DCL.UI.Sidebar.HelpMenu public class HelpMenuController : ControllerBase { private readonly IMVCManager mvcManager; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly SupportRequestService supportRequestService; private UniTaskCompletionSource? closeViewTask; @@ -19,7 +19,7 @@ public class HelpMenuController : ControllerBase public override CanvasOrdering.SortingLayer Layer => CanvasOrdering.SortingLayer.POPUP; - public HelpMenuController(ViewFactoryMethod viewFactory, IMVCManager mvcManager, IWebBrowser webBrowser, SupportRequestService supportRequestService) + public HelpMenuController(ViewFactoryMethod viewFactory, IMVCManager mvcManager, UnityAppWebBrowser webBrowser, SupportRequestService supportRequestService) : base(viewFactory) { this.mvcManager = mvcManager; @@ -69,7 +69,7 @@ private void OnMouseAndKeyControlsClicked() private void OnFaqClicked() { CloseView(); - webBrowser.OpenUrl(DecentralandUrl.Faqs); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.Faqs); } private void OnContactSupportClicked() @@ -81,7 +81,7 @@ private void OnContactSupportClicked() private void OnDiscordClicked() { CloseView(); - webBrowser.OpenUrl(DecentralandUrl.Discord); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.Discord); } private void CloseView() => closeViewTask?.TrySetResult(); diff --git a/Explorer/Assets/DCL/UI/Sidebar/SidebarController.cs b/Explorer/Assets/DCL/UI/Sidebar/SidebarController.cs index 7a5e013f47b..1970e5fba29 100644 --- a/Explorer/Assets/DCL/UI/Sidebar/SidebarController.cs +++ b/Explorer/Assets/DCL/UI/Sidebar/SidebarController.cs @@ -49,7 +49,7 @@ public class SidebarController : ControllerBase private readonly IMVCManager mvcManager; private readonly SidebarProfileButtonPresenter profileButtonPresenter; private readonly SmartWearablesSideBarTooltipController smartWearablesTooltipController; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IChatHistory chatHistory; private readonly ISelfProfile selfProfile; private readonly IRealmData realmData; @@ -89,7 +89,7 @@ public SidebarController(ViewFactoryMethod viewFactory, IMVCManager mvcManager, SidebarProfileButtonPresenter profileButtonPresenter, SmartWearablesSideBarTooltipController smartWearablesTooltipController, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IChatHistory chatHistory, ISelfProfile selfProfile, IRealmData realmData, @@ -304,7 +304,7 @@ async UniTaskVoid OpenReferralWebsiteAsync(CancellationToken ct) .AppendPath(URLPath.FromString($"profile/accounts/{myProfile.UserId}/referral")) .Build(); - webBrowser.OpenUrl(url); + webBrowser.OpenUrlMainThreadOnly(url); } catch (OperationCanceledException) { } catch (Exception e) { ReportHub.LogException(e, ReportCategory.PROFILE); } @@ -451,7 +451,7 @@ private void OnMarketplaceButtonClicked() urlBuilder.Clear(); urlBuilder.AppendDomain(URLDomain.FromString(decentralandUrlsSource.Url(DecentralandUrl.Market))); urlBuilder.AppendParameter(marketplaceSourceParam); - webBrowser.OpenUrl(urlBuilder.Build()); + webBrowser.OpenUrlMainThreadOnly(urlBuilder.Build()); } private async UniTaskVoid OpenPanelAsync( diff --git a/Explorer/Assets/DCL/UI/SystemMenu/SystemSectionPresenter.cs b/Explorer/Assets/DCL/UI/SystemMenu/SystemSectionPresenter.cs index 98083e89bc2..13a4ae65b08 100644 --- a/Explorer/Assets/DCL/UI/SystemMenu/SystemSectionPresenter.cs +++ b/Explorer/Assets/DCL/UI/SystemMenu/SystemSectionPresenter.cs @@ -21,7 +21,7 @@ public class SystemSectionPresenter : IDisposable public event Action? OnClosed; private readonly SystemMenuView view; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly IWeb3Authenticator web3Authenticator; private readonly IUserInAppInitializationFlow userInAppInitializationFlow; private readonly IWeb3IdentityCache web3IdentityCache; @@ -36,7 +36,7 @@ public SystemSectionPresenter( SystemMenuView view, World world, Entity playerEntity, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, IWeb3Authenticator web3Authenticator, IUserInAppInitializationFlow userInAppInitializationFlow, IProfileCache profileCache, @@ -97,10 +97,10 @@ private void CloseView() } private void ShowTermsOfService() => - webBrowser.OpenUrl(DecentralandUrl.TermsOfUse); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.TermsOfUse); private void ShowPrivacyPolicy() => - webBrowser.OpenUrl(DecentralandUrl.PrivacyPolicy); + webBrowser.OpenUrlMainThreadOnly(DecentralandUrl.PrivacyPolicy); private void Logout() diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index f098716318f..f7c3ff905f3 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -27,7 +27,7 @@ public class DappDeepLinkAuthenticator : IWeb3Authenticator private const double IDENTITY_EXPIRATION_PERIOD_FALLBACK_IN_DAYS = 30; private const int DEEPLINK_TIMEOUT_SECONDS = 300; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly URLAddress authApiUrl; private readonly URLAddress signatureWebAppUrl; private readonly IWeb3AccountFactory web3AccountFactory; @@ -37,7 +37,7 @@ public class DappDeepLinkAuthenticator : IWeb3Authenticator private readonly URLBuilder urlBuilder = new (); public DappDeepLinkAuthenticator( - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, URLAddress authApiUrl, URLAddress signatureWebAppUrl, IWeb3AccountFactory web3AccountFactory, @@ -72,12 +72,11 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio if (string.IsNullOrEmpty(createRequestResponse.requestId)) throw new Web3Exception("Cannot solve auth request id"); - // OpenUrl routes through Application.OpenURL, which must run on the main thread. await UniTask.SwitchToMainThread(ct); string url = $"{signatureWebAppUrl}/{createRequestResponse.requestId}?loginMethod={payload.Method}&flow=deeplink"; - webBrowser.OpenUrl(url); + webBrowser.OpenUrlMainThreadOnly(url); // The browser builds and stores the AuthIdentity, then opens decentraland://?signin={identityId}, // which is delivered here through the deep link pipeline. diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs index c9d3e70273d..da90bcb6718 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs @@ -27,7 +27,7 @@ public partial class DappWeb3EthereumApi : IEthereumApi private const int TIMEOUT_SECONDS = 30; private const int RPC_BUFFER_SIZE = 50000; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly URLAddress authApiUrl; private readonly URLAddress signatureWebAppUrl; private readonly URLDomain rpcServerUrl; @@ -49,7 +49,7 @@ public partial class DappWeb3EthereumApi : IEthereumApi private DCLWebSocket? rpcWebSocket; private UniTaskCompletionSource? signatureOutcomeTask; - public DappWeb3EthereumApi(IWebBrowser webBrowser, + public DappWeb3EthereumApi(UnityAppWebBrowser webBrowser, URLAddress authApiUrl, URLAddress signatureWebAppUrl, URLDomain rpcServerUrl, @@ -424,7 +424,7 @@ private async UniTask RequestSignatureAsync(string requestId, LoginMethod? ? $"{signatureWebAppUrl}/{requestId}?loginMethod={method.Value}" : $"{signatureWebAppUrl}/{requestId}"; - webBrowser.OpenUrl(url); + webBrowser.OpenUrlMainThreadOnly(url); signatureOutcomeTask?.TrySetCanceled(ct); signatureOutcomeTask = new UniTaskCompletionSource(); From 3df3783ee905b55ab68d9b4a6e45ae07a867efb4 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 20:09:47 +0200 Subject: [PATCH 21/54] removed no-op logout across authentificators --- .../Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs | 4 ++-- Explorer/Assets/DCL/PluginSystem/Global/SidebarPlugin.cs | 4 ++-- Explorer/Assets/DCL/UI/Profiles/ProfileMenuController.cs | 4 ++-- .../Assets/DCL/UI/SystemMenu/SystemSectionPresenter.cs | 4 ++-- .../DCL/Web3/Authenticators/ICompositeWeb3Provider.cs | 7 +++++++ .../Assets/DCL/Web3/Authenticators/IWeb3Authenticator.cs | 2 -- .../Implementations/CompositeWeb3Provider.cs | 6 +++++- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 3 --- .../Implementations/Dapp/DappWeb3EthereumApi.Default.cs | 3 --- .../Implementations/Dapp/DappWeb3EthereumApi.cs | 3 --- .../Implementations/PrivateKeyAuthenticator.cs | 3 --- .../Implementations/RandomGeneratedWeb3Authenticator.cs | 3 --- .../Implementations/TokenFileAuthenticator.cs | 3 --- .../Implementations/TrackedTokenFileAuthenticator.cs | 3 --- 14 files changed, 20 insertions(+), 32 deletions(-) diff --git a/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs index cb642ae91e4..65b65f6c78c 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs @@ -103,7 +103,7 @@ public class ExplorePanelPlugin : IDCLGlobalPlugin private readonly IProfileRepository profileRepository; private readonly IWebRequestController webRequestController; private readonly UnityAppWebBrowser webBrowser; - private readonly IWeb3Authenticator web3Authenticator; + private readonly ICompositeWeb3Provider web3Authenticator; private readonly IUserInAppInitializationFlow userInAppInitializationFlow; private readonly IProfileCache profileCache; private readonly Arch.Core.World globalWorld; @@ -92,7 +92,7 @@ public SidebarPlugin( IProfileRepository profileRepository, IWebRequestController webRequestController, UnityAppWebBrowser webBrowser, - IWeb3Authenticator web3Authenticator, + ICompositeWeb3Provider web3Authenticator, IUserInAppInitializationFlow userInAppInitializationFlow, IProfileCache profileCache, Arch.Core.World globalWorld, diff --git a/Explorer/Assets/DCL/UI/Profiles/ProfileMenuController.cs b/Explorer/Assets/DCL/UI/Profiles/ProfileMenuController.cs index efa6c083291..662c9888a73 100644 --- a/Explorer/Assets/DCL/UI/Profiles/ProfileMenuController.cs +++ b/Explorer/Assets/DCL/UI/Profiles/ProfileMenuController.cs @@ -21,7 +21,7 @@ public class ProfileMenuController : ControllerBase private readonly World world; private readonly Entity playerEntity; private readonly UnityAppWebBrowser webBrowser; - private readonly IWeb3Authenticator web3Authenticator; + private readonly ICompositeWeb3Provider web3Authenticator; private readonly IUserInAppInitializationFlow userInAppInitializationFlow; private readonly IProfileCache profileCache; private readonly IPassportBridge passportBridge; @@ -37,7 +37,7 @@ public ProfileMenuController( World world, Entity playerEntity, UnityAppWebBrowser webBrowser, - IWeb3Authenticator web3Authenticator, + ICompositeWeb3Provider web3Authenticator, IUserInAppInitializationFlow userInAppInitializationFlow, IProfileCache profileCache, IPassportBridge passportBridge, diff --git a/Explorer/Assets/DCL/UI/SystemMenu/SystemSectionPresenter.cs b/Explorer/Assets/DCL/UI/SystemMenu/SystemSectionPresenter.cs index 13a4ae65b08..0d180a8fbd7 100644 --- a/Explorer/Assets/DCL/UI/SystemMenu/SystemSectionPresenter.cs +++ b/Explorer/Assets/DCL/UI/SystemMenu/SystemSectionPresenter.cs @@ -22,7 +22,7 @@ public class SystemSectionPresenter : IDisposable private readonly SystemMenuView view; private readonly UnityAppWebBrowser webBrowser; - private readonly IWeb3Authenticator web3Authenticator; + private readonly ICompositeWeb3Provider web3Authenticator; private readonly IUserInAppInitializationFlow userInAppInitializationFlow; private readonly IWeb3IdentityCache web3IdentityCache; private readonly IProfileCache profileCache; @@ -37,7 +37,7 @@ public SystemSectionPresenter( World world, Entity playerEntity, UnityAppWebBrowser webBrowser, - IWeb3Authenticator web3Authenticator, + ICompositeWeb3Provider web3Authenticator, IUserInAppInitializationFlow userInAppInitializationFlow, IProfileCache profileCache, IWeb3IdentityCache web3IdentityCache, diff --git a/Explorer/Assets/DCL/Web3/Authenticators/ICompositeWeb3Provider.cs b/Explorer/Assets/DCL/Web3/Authenticators/ICompositeWeb3Provider.cs index 045828076e4..ca5b51de044 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/ICompositeWeb3Provider.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/ICompositeWeb3Provider.cs @@ -1,4 +1,5 @@ using Cysharp.Threading.Tasks; +using System.Threading; namespace DCL.Web3.Authenticators { @@ -20,6 +21,12 @@ public interface ICompositeWeb3Provider : IWeb3Authenticator, IEthereumApi, IOtp /// AuthProvider CurrentProvider { set; } + /// + /// Clears the local session (identity cache, analytics identity) and releases + /// provider-side login resources where the current provider holds any. + /// + UniTask LogoutAsync(CancellationToken ct); + /// /// Returns true if ThirdWeb OTP method is currently selected /// diff --git a/Explorer/Assets/DCL/Web3/Authenticators/IWeb3Authenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/IWeb3Authenticator.cs index f52373cc2de..ef75d0cf166 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/IWeb3Authenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/IWeb3Authenticator.cs @@ -8,8 +8,6 @@ namespace DCL.Web3.Authenticators public interface IWeb3Authenticator : IDisposable { UniTask LoginAsync(LoginPayload payload, CancellationToken ct); - - UniTask LogoutAsync(CancellationToken ct); } public enum LoginMethod diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs index 07e87728317..7d607182aaa 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs @@ -73,7 +73,11 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio public async UniTask LogoutAsync(CancellationToken ct) { analytics.Identify(null); - await currentAuthenticator.LogoutAsync(ct); + + // ThirdWeb is the only provider holding a login session of its own; the others have nothing to release. + if (IsThirdWebOTP) + await thirdWebAuth.LogoutAsync(ct); + identityCache.Clear(); } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index f7c3ff905f3..084562bdcec 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -85,9 +85,6 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio return await FetchIdentityByIdAsync(identityId, ct); } - public UniTask LogoutAsync(CancellationToken ct) => - UniTask.CompletedTask; - /// /// Mints a sign-in requestId via POST {authApiUrl}/requests. /// The browser later recovers the request by that id to drive the wallet signature. diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.Default.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.Default.cs index cd0f55b6ec0..66dcf5c923c 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.Default.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.Default.cs @@ -65,9 +65,6 @@ public UniTask SendAsync(EthApiRequest request, Web3RequestSourc // IWeb3Authenticator public UniTask LoginAsync(LoginPayload payload, CancellationToken ct) => origin.LoginAsync(payload, ct); - - public UniTask LogoutAsync(CancellationToken ct) => - origin.LogoutAsync(ct); } } } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs index da90bcb6718..d2fcaabddfa 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs @@ -212,9 +212,6 @@ private async UniTask LoginAsync(LoginPayload payload, Cancellati } } - private async UniTask LogoutAsync(CancellationToken ct) => - await DisconnectFromAuthApiAsync(); - private async UniTask DisconnectFromAuthApiAsync() { if (authApiWebSocket is { Connected: true }) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/PrivateKeyAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/PrivateKeyAuthenticator.cs index a5cf0c9f72c..40a0333c38a 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/PrivateKeyAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/PrivateKeyAuthenticator.cs @@ -108,8 +108,5 @@ private static AuthChain CreateAuthChain(IWeb3Account account, IWeb3Account ephe return authChain; } - - public UniTask LogoutAsync(CancellationToken ct) => - UniTask.CompletedTask; } } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/RandomGeneratedWeb3Authenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/RandomGeneratedWeb3Authenticator.cs index c0d48e61e4c..4beec1ca5ee 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/RandomGeneratedWeb3Authenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/RandomGeneratedWeb3Authenticator.cs @@ -49,9 +49,6 @@ public UniTask LoginAsync(LoginPayload payload, CancellationToken ).AsUniTaskResult(); } - public UniTask LogoutAsync(CancellationToken ct) => - UniTask.CompletedTask; - public UniTask RequestTransferAsync(string giftUrn, string recipientAddress, CancellationToken ct) { throw new NotImplementedException(); diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TokenFileAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TokenFileAuthenticator.cs index ea2d40aa707..7f3e9f933b8 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TokenFileAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TokenFileAuthenticator.cs @@ -96,9 +96,6 @@ private async UniTask LoginAsync(CancellationToken ct) IWeb3Identity.Web3IdentitySource.TokenFile); } - public UniTask LogoutAsync(CancellationToken ct) => - UniTask.CompletedTask; - public UniTask RequestTransferAsync(string giftUrn, string recipientAddress, CancellationToken ct) { throw new NotImplementedException(); diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TrackedTokenFileAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TrackedTokenFileAuthenticator.cs index 52f184a1f51..a5adc9a9442 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TrackedTokenFileAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TrackedTokenFileAuthenticator.cs @@ -62,9 +62,6 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio } } - public UniTask LogoutAsync(CancellationToken ct) => - inner.LogoutAsync(ct); - public void Dispose() => inner.Dispose(); } From 918e9c845df4f4b20b91da007664b3045fa89952 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 20:46:32 +0200 Subject: [PATCH 22/54] add retries and exception handling --- ...entityVerificationDappDeepLinkAuthState.cs | 1 + .../DeeplinkSigninRetrievalException.cs | 38 +++++++++++++++++++ .../DeeplinkSigninRetrievalException.cs.meta | 2 + .../Dapp/DappDeepLinkAuthenticator.cs | 11 +++++- Explorer/Web3.csproj.DotSettings | 2 + 5 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Exceptions/DeeplinkSigninRetrievalException.cs create mode 100644 Explorer/Assets/DCL/Web3/Authenticators/Exceptions/DeeplinkSigninRetrievalException.cs.meta create mode 100644 Explorer/Web3.csproj.DotSettings diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs index 46e3b905181..f3915e8591d 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs @@ -60,6 +60,7 @@ public override void Exit() TimeoutException ex => new SpanErrorInfo("Deep-link sign-in timed out waiting for the browser to deliver the signin deep link", ex), SignatureExpiredException ex => new SpanErrorInfo("Web3 signature expired during deep-link authentication", ex), Web3SignatureException ex => new SpanErrorInfo("Web3 signature validation failed during deep-link authentication", ex), + DeeplinkSigninRetrievalException ex => new SpanErrorInfo($"Signin identity retrieval failed: {ex.Reason}", ex), Web3Exception ex => new SpanErrorInfo("Connection error during deep-link authentication flow", ex), Exception ex => new SpanErrorInfo("Unexpected error during deep-link authentication flow", ex), }; diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Exceptions/DeeplinkSigninRetrievalException.cs b/Explorer/Assets/DCL/Web3/Authenticators/Exceptions/DeeplinkSigninRetrievalException.cs new file mode 100644 index 00000000000..eb8b22d7350 --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/Exceptions/DeeplinkSigninRetrievalException.cs @@ -0,0 +1,38 @@ +namespace DCL.Web3.Authenticators +{ + /// + /// Raised when fetching the identity minted by the deep-link sign-in flow + /// (GET /identities/{identityId}) fails with a known status code. + /// + public class DeeplinkSigninRetrievalException : Web3Exception + { + public enum ErrorReason + { + /// 404: the identity does not exist or has already been retrieved (identities are single-use). + NOT_FOUND, + + /// 410: the identity expired before retrieval (server-side TTL is 15 minutes). + EXPIRED, + + /// 403: the retrieval came from a different IP than the one that stored the identity (commonly a VPN or private relay). + IP_MISMATCH, + } + + public ErrorReason Reason { get; } + + public DeeplinkSigninRetrievalException(ErrorReason reason, string identityId) + : base(MessageFor(reason, identityId)) + { + Reason = reason; + } + + private static string MessageFor(ErrorReason reason, string identityId) => + reason switch + { + ErrorReason.NOT_FOUND => $"Signin identity {identityId} was not found or was already retrieved", + ErrorReason.EXPIRED => $"Signin identity {identityId} expired before it was retrieved", + ErrorReason.IP_MISMATCH => $"Signin identity {identityId} was stored from a different IP: a VPN or private relay is likely interfering", + _ => $"Signin identity {identityId} retrieval failed: {reason}", + }; + } +} diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Exceptions/DeeplinkSigninRetrievalException.cs.meta b/Explorer/Assets/DCL/Web3/Authenticators/Exceptions/DeeplinkSigninRetrievalException.cs.meta new file mode 100644 index 00000000000..5e61d4568ed --- /dev/null +++ b/Explorer/Assets/DCL/Web3/Authenticators/Exceptions/DeeplinkSigninRetrievalException.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ecb0ca6ce100dc142a3deca47de0206b \ No newline at end of file diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index 084562bdcec..d32e23f46ea 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -96,7 +96,7 @@ private async UniTask CreateSigninRequestAsync(string urlBuilder.AppendDomain(URLDomain.FromString(authApiUrl)) .AppendPath(new URLPath("requests")); - var commonArguments = new CommonArguments(urlBuilder.Build()); + var commonArguments = new CommonArguments(urlBuilder.Build(), RetryPolicy.Enforce()); string body = JsonConvert.SerializeObject(new SigninRequestDto { @@ -140,7 +140,14 @@ private async UniTask FetchIdentityByIdAsync(string identi var commonArguments = new CommonArguments(urlBuilder.Build()); IdentityAuthResponseDto json = await webRequestController.GetAsync(commonArguments, ct, ReportCategory.AUTHENTICATION) - .CreateFromNewtonsoftJsonAsync(); + .CreateFromNewtonsoftJsonAsync() + .WithCustomExceptionAsync(e => e.ResponseCode switch + { + 404 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.NOT_FOUND, identityId), + 410 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.EXPIRED, identityId), + 403 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.IP_MISMATCH, identityId), + _ => (Exception)e, + }); string? signerAddress = null; string? ephemeralPayload = null; diff --git a/Explorer/Web3.csproj.DotSettings b/Explorer/Web3.csproj.DotSettings new file mode 100644 index 00000000000..869205a4df0 --- /dev/null +++ b/Explorer/Web3.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file From 289960de8e2ab5c68f56b3b450e7619cc2d77323 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 21:44:22 +0200 Subject: [PATCH 23/54] harden deep-link sign-in against stale signin ids, fix local-scene login hang, abort pending dapp signature on logout Addresses external review findings on the deep-link login refactor: - A signin identityId buffered before a login attempt minted its request (stale bridge file, cancelled/timed-out previous attempt, injected decentraland:// event) could complete a later attempt with another session's identity. LoginAsync now drops any pre-existing id before minting, and WaitForSigninAsync consumes the id on success, timeout and cancellation alike, scoping deep-link delivery to one attempt. - Deep-link listening was fully disabled under --local-scene, but the production wallet flow now depends on it, so login hung for 300s in exactly the sessions where web3 flows are tested most. The sentinel now runs in every mode; local-scene only opts out of navigation routing (teleports would break the scene under test). - After the login/eth-api provider split, logout stopped reaching the auth-api socket, so a browser signature approved after logout could still complete under the old session. Dapp logout now disconnects and cancels the pending confirmation before clearing the identity cache (local cancellation only - the server-side identity stays valid). Co-Authored-By: Claude Opus 4.6 --- .../Global/Dynamic/DynamicWorldContainer.cs | 12 ++++++------ .../DeepLinkHandleImplementation.cs | 12 +++++++++++- .../Implementations/CompositeWeb3Provider.cs | 6 +++++- .../Dapp/DappDeepLinkAuthenticator.cs | 17 +++++++++++------ .../Implementations/Dapp/DappWeb3EthereumApi.cs | 6 +++++- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs index 6044583bfab..a18085381ad 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -472,12 +472,12 @@ await MapRendererContainer GenericUserProfileContextMenuSettings genericUserProfileContextMenuSettingsSo = (await assetsProvisioner.ProvideMainAssetAsync(dynamicSettings.GenericUserProfileContextMenuSettings, ct)).Value; CommunityVoiceChatContextMenuConfiguration communityVoiceChatContextMenuSettingsSo = (await assetsProvisioner.ProvideMainAssetAsync(dynamicSettings.CommunityVoiceChatContextMenuSettings, ct)).Value; - // Local scene development scenes are excluded from deeplink runtime handling logic - if (!appArgs.HasFlag(AppArgsFlags.LOCAL_SCENE)) - { - var deepLinkHandleImplementation = new DeepLinkHandle(dynamicWorldParams.StartParcel, chatContainer.ChatTeleporter, ct, communitiesDataService, bootstrapContainer.DeeplinkSigninIdentityId); - deepLinkHandleImplementation.StartListenForDeepLinksAsync(ct).Forget(); - } + // Deep link listening stays alive in every mode so browser sign-in can complete; local scene + // development only opts out of navigation routing (teleports would break the scene under test). + var deepLinkHandleImplementation = new DeepLinkHandle(dynamicWorldParams.StartParcel, chatContainer.ChatTeleporter, ct, communitiesDataService, bootstrapContainer.DeeplinkSigninIdentityId, + routeNavigationDeepLinks: !appArgs.HasFlag(AppArgsFlags.LOCAL_SCENE)); + + deepLinkHandleImplementation.StartListenForDeepLinksAsync(ct).Forget(); IMVCManagerMenusAccessFacade menusAccessFacade = new MVCManagerMenusAccessFacade( uiShellContainer.MvcManager, diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index 563c07e5932..61ddff5dd2d 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -2,6 +2,7 @@ using Cysharp.Threading.Tasks; using DCL.Chat.Commands; using DCL.Communities; +using DCL.Diagnostics; using DCL.RealmNavigation; using DCL.Utilities; using Global.AppArgs; @@ -17,14 +18,17 @@ public class DeepLinkHandle : IDeepLinkHandle private readonly CancellationToken token; private readonly CommunityDataService communityDataService; private readonly ReactiveProperty deeplinkSigninIdentityId; + private readonly bool routeNavigationDeepLinks; - public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, CancellationToken token, CommunityDataService communityDataService, ReactiveProperty deeplinkSigninIdentityId) + public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, CancellationToken token, CommunityDataService communityDataService, ReactiveProperty deeplinkSigninIdentityId, + bool routeNavigationDeepLinks) { this.startParcel = startParcel; this.chatTeleporter = chatTeleporter; this.token = token; this.communityDataService = communityDataService; this.deeplinkSigninIdentityId = deeplinkSigninIdentityId; + this.routeNavigationDeepLinks = routeNavigationDeepLinks; } public DeepLinkHandleResult HandleDeepLink(DeepLink deeplink) @@ -38,6 +42,12 @@ public DeepLinkHandleResult HandleDeepLink(DeepLink deeplink) return DeepLinkHandleResult.Consumed; } + if (!routeNavigationDeepLinks) + { + ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"navigation deep link routing is disabled, dropping: {deeplink}"); + return DeepLinkHandleResult.Consumed; + } + Vector2Int? position = PositionFrom(deeplink); URLDomain? realm = RealmFrom(deeplink); string? communityId = CommunityFrom(deeplink); diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs index 7d607182aaa..6c9856791f9 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/CompositeWeb3Provider.cs @@ -74,9 +74,13 @@ public async UniTask LogoutAsync(CancellationToken ct) { analytics.Identify(null); - // ThirdWeb is the only provider holding a login session of its own; the others have nothing to release. + // ThirdWeb is the only provider holding a login session of its own. if (IsThirdWebOTP) await thirdWebAuth.LogoutAsync(ct); + else + // Abort any in-flight browser signature confirmation so an approval arriving + // after logout cannot complete under the logged-out session. + await dappEthereumApi.DisconnectFromAuthApiAsync(); identityCache.Clear(); } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index d32e23f46ea..ad195683efd 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -58,6 +58,10 @@ public void Dispose() { } public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) { + // A signin id stored before this attempt minted its request cannot belong to it: drop it + // so a stale or foreign deep link does not complete this login with another session's identity. + deeplinkSigninIdentityId.Value = null; + // The ephemeral address is embedded in the signed message so the server can mint a well-formed request from it. var ephemeralAccount = web3AccountFactory.CreateRandomAccount(); @@ -122,12 +126,13 @@ private async UniTask WaitForSigninAsync(CancellationToken ct) completion.TrySetResult(identityId); }, ct); - string identityId = await completionSource.Task.Timeout(TimeSpan.FromSeconds(DEEPLINK_TIMEOUT_SECONDS), DelayType.Realtime).AttachExternalCancellation(ct); - - // Consume the id so a future login does not resolve against this same signin. - deeplinkSigninIdentityId.Value = null; - - return identityId; + try { return await completionSource.Task.Timeout(TimeSpan.FromSeconds(DEEPLINK_TIMEOUT_SECONDS), DelayType.Realtime).AttachExternalCancellation(ct); } + finally + { + // Consume the id on success, timeout and cancellation alike, so a later + // login attempt never resolves against a signin delivered for this one. + deeplinkSigninIdentityId.Value = null; + } } private async UniTask FetchIdentityByIdAsync(string identityId, CancellationToken ct) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs index d2fcaabddfa..31c0ef22019 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs @@ -212,7 +212,11 @@ private async UniTask LoginAsync(LoginPayload payload, Cancellati } } - private async UniTask DisconnectFromAuthApiAsync() + /// + /// Drops the auth-api socket and aborts any in-flight browser signature confirmation, + /// so an approval arriving later cannot complete under the previous session. + /// + public async UniTask DisconnectFromAuthApiAsync() { if (authApiWebSocket is { Connected: true }) await authApiWebSocket.DisconnectAsync(); From 7f4bdc211e12d608142f19383a01dc37583cdc79 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 2 Jul 2026 23:38:23 +0200 Subject: [PATCH 24/54] fixed last comment --- .../Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index 61ddff5dd2d..516f3a58a57 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -37,7 +37,7 @@ public DeepLinkHandleResult HandleDeepLink(DeepLink deeplink) if (!string.IsNullOrEmpty(signin)) { - // The property retains the id, so a login flow that subscribes later still receives it. + // The id persists in the property until it is overwritten or cleared. deeplinkSigninIdentityId.Value = signin; return DeepLinkHandleResult.Consumed; } From b374e1aa3affae00ec0231493470699d5f83a234 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Tue, 7 Jul 2026 15:46:08 -0300 Subject: [PATCH 25/54] fix warningsat DappDeepLinkAuthenticator --- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index ad195683efd..3132cc17bcb 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -78,7 +78,7 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio await UniTask.SwitchToMainThread(ct); - string url = $"{signatureWebAppUrl}/{createRequestResponse.requestId}?loginMethod={payload.Method}&flow=deeplink"; + var url = $"{signatureWebAppUrl}/{createRequestResponse.requestId}?loginMethod={payload.Method}&flow=deeplink"; webBrowser.OpenUrlMainThreadOnly(url); @@ -151,19 +151,17 @@ private async UniTask FetchIdentityByIdAsync(string identi 404 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.NOT_FOUND, identityId), 410 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.EXPIRED, identityId), 403 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.IP_MISMATCH, identityId), - _ => (Exception)e, + _ => e, }); string? signerAddress = null; string? ephemeralPayload = null; foreach (AuthLink authLink in json.identity.authChain) - { if (authLink.type == AuthLinkType.SIGNER) signerAddress = authLink.payload; else if (authLink.type is AuthLinkType.ECDSA_EPHEMERAL or AuthLinkType.ECDSA_EIP_1654_EPHEMERAL) ephemeralPayload = authLink.payload; - } if (signerAddress is not { Length: > 0 }) throw new Web3Exception($"Sign-in identity {identityId} has no SIGNER link in its auth chain"); @@ -196,6 +194,7 @@ private static string CreateEphemeralMessage(IWeb3Account ephemeralAccount, Date private struct SigninRequestDto { public string method; + // ReSharper disable once InconsistentNaming public object[] @params; } From 60361878b21ed6e5dea76c858abac6063b91d506 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Tue, 7 Jul 2026 16:01:44 -0300 Subject: [PATCH 26/54] fix null check warnings at Bootstrapper --- .../Global/Dynamic/Bootstraper.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/Bootstraper.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/Bootstraper.cs index 58fa3365957..a5cce7fc25f 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/Bootstraper.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/Bootstraper.cs @@ -105,21 +105,21 @@ public async UniTask PreInitializeSetupAsync(CancellationToken token) RealmData realmData, Entity playerEntity, ISystemMemoryCap memoryCap, - IAppArgs appArgs, + IAppArgs args, CancellationToken ct ) => await StaticContainer.CreateAsync( bootstrapContainer.Analytics, bootstrapContainer.DecentralandUrlsSource, realmData, - bootstrapContainer.AssetsProvisioner, + bootstrapContainer.AssetsProvisioner!, bootstrapContainer.ReportHandlingSettings, debugContainerBuilder, webRequestsContainer, pluginSettingsContainer, bootstrapContainer.DiagnosticsContainer, - bootstrapContainer.IdentityCache, - bootstrapContainer.CompositeWeb3Provider, + bootstrapContainer.IdentityCache!, + bootstrapContainer.CompositeWeb3Provider!, bootstrapContainer.LaunchMode, bootstrapContainer.UseRemoteAssetBundles, world, @@ -130,7 +130,7 @@ await StaticContainer.CreateAsync( diskCache, partialsDiskCache, ct, - appArgs + args ); public async UniTask<(DynamicWorldContainer?, bool)> LoadDynamicWorldContainerAsync( @@ -142,7 +142,7 @@ await StaticContainer.CreateAsync( AudioClipConfig backgroundMusic, WorldInfoTool worldInfoTool, Entity playerEntity, - IAppArgs appArgs, + IAppArgs args, ICoroutineRunner coroutineRunner, DCLVersion dclVersion, CancellationToken ct) @@ -150,13 +150,13 @@ await StaticContainer.CreateAsync( dynamicWorldDependencies = new DynamicWorldDependencies ( staticContainer.DebugContainerBuilder, - appArgs, - bootstrapContainer.AssetsProvisioner, + args, + bootstrapContainer.AssetsProvisioner!, staticContainer, pluginSettingsContainer, dynamicSettings, bootstrapContainer.CompositeWeb3Provider!, - bootstrapContainer.IdentityCache, + bootstrapContainer.IdentityCache!, splashScreen, worldInfoTool ); @@ -182,7 +182,7 @@ await StaticContainer.CreateAsync( backgroundMusic, world, playerEntity, - appArgs, + args, coroutineRunner, dclVersion, realmUrls, @@ -250,7 +250,7 @@ Entity playerEntity SceneSharedContainer sceneSharedContainer = SceneSharedContainer.Create( in staticContainer, bootstrapContainer.DecentralandUrlsSource, - bootstrapContainer.IdentityCache, + bootstrapContainer.IdentityCache!, staticContainer.WebRequestsContainer.SceneWebRequestController, dynamicWorldContainer.RealmController.RealmData, dynamicWorldContainer.ProfileRepository, From 8538e1142c3f95d934e6b690f65f011014d0d419 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Wed, 8 Jul 2026 14:46:44 -0300 Subject: [PATCH 27/54] fix multiple explorer instances authentication: defer deep link consumption when no login is active --- .../Global/Dynamic/BootstrapContainer.cs | 11 ++++- .../Global/Dynamic/DynamicWorldContainer.cs | 2 +- .../DCL/RuntimeDeepLink/DeepLinkHandle.cs | 7 ++++ .../DeepLinkHandleImplementation.cs | 11 ++++- .../DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 42 +++++++++++++++++-- .../Dapp/DappDeepLinkAuthenticator.cs | 13 +++++- 6 files changed, 77 insertions(+), 9 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index 2c677507f7e..20805858242 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -50,6 +50,11 @@ public class BootstrapContainer : DCLGlobalContainer public IWeb3IdentityCache? IdentityCache { get; private set; } public ICompositeWeb3Provider? CompositeWeb3Provider { get; private set; } public ReactiveProperty DeeplinkSigninIdentityId { get; } = new (null); + + // Raised by the deep-link login flow while it waits for a signin; the deep-link pipeline only + // consumes a signin (and deletes the shared bridge file) while this is true, so a concurrent idle + // Explorer instance doesn't steal the signin from the instance that is actually logging in. + public ReactiveProperty DeeplinkLoginAwaitingSignin { get; } = new (false); public AnalyticsContainer Analytics { get; private set; } public DebugSettings.DebugSettings DebugSettings { get; private set; } public VolumeBus VolumeBus { get; private set; } @@ -131,7 +136,7 @@ await bootstrapContainer.InitializeContainerAsync deeplinkSigninIdentityId) + ReactiveProperty deeplinkSigninIdentityId, + ReactiveProperty deeplinkLoginAwaitingSignin) { int? identityExpirationDuration = appArgs.TryGetValue(AppArgsFlags.IDENTITY_EXPIRATION_DURATION, out string? v) ? int.Parse(v!) @@ -211,6 +217,7 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies( web3AccountFactory, webRequestController, deeplinkSigninIdentityId, + deeplinkLoginAwaitingSignin, identityExpirationDuration ); diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs index a18085381ad..bcccc2e9ac7 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -475,7 +475,7 @@ await MapRendererContainer // Deep link listening stays alive in every mode so browser sign-in can complete; local scene // development only opts out of navigation routing (teleports would break the scene under test). var deepLinkHandleImplementation = new DeepLinkHandle(dynamicWorldParams.StartParcel, chatContainer.ChatTeleporter, ct, communitiesDataService, bootstrapContainer.DeeplinkSigninIdentityId, - routeNavigationDeepLinks: !appArgs.HasFlag(AppArgsFlags.LOCAL_SCENE)); + bootstrapContainer.DeeplinkLoginAwaitingSignin, routeNavigationDeepLinks: !appArgs.HasFlag(AppArgsFlags.LOCAL_SCENE)); deepLinkHandleImplementation.StartListenForDeepLinksAsync(ct).Forget(); diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs index 5bff79a533a..926c7afbb39 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs @@ -4,6 +4,13 @@ public enum DeepLinkHandleResult { Consumed, NoMatches, + + /// + /// A signin deep link arrived while no login flow was waiting for it. It is left untouched so the + /// instance actually logging in can claim it, rather than a concurrent idle Explorer instance + /// consuming and deleting the shared bridge file first. Kept until claimed (bounded by a timeout). + /// + Deferred, } public interface IDeepLinkHandle diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index 516f3a58a57..7859cdcd1eb 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -18,16 +18,18 @@ public class DeepLinkHandle : IDeepLinkHandle private readonly CancellationToken token; private readonly CommunityDataService communityDataService; private readonly ReactiveProperty deeplinkSigninIdentityId; + private readonly IReadonlyReactiveProperty loginAwaitingSignin; private readonly bool routeNavigationDeepLinks; public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, CancellationToken token, CommunityDataService communityDataService, ReactiveProperty deeplinkSigninIdentityId, - bool routeNavigationDeepLinks) + IReadonlyReactiveProperty loginAwaitingSignin, bool routeNavigationDeepLinks) { this.startParcel = startParcel; this.chatTeleporter = chatTeleporter; this.token = token; this.communityDataService = communityDataService; this.deeplinkSigninIdentityId = deeplinkSigninIdentityId; + this.loginAwaitingSignin = loginAwaitingSignin; this.routeNavigationDeepLinks = routeNavigationDeepLinks; } @@ -37,6 +39,13 @@ public DeepLinkHandleResult HandleDeepLink(DeepLink deeplink) if (!string.IsNullOrEmpty(signin)) { + // Only a login flow actively awaiting a signin may consume it. The bridge file is a single + // shared handoff point, so with several Explorer instances open an idle one would otherwise + // read and delete the file first, "stealing" the signin from the instance that is actually + // logging in. Leaving it in place lets the awaiting instance claim it instead. + if (!loginAwaitingSignin.Value) + return DeepLinkHandleResult.Deferred; + // The id persists in the property until it is overwritten or cleared. deeplinkSigninIdentityId.Value = signin; return DeepLinkHandleResult.Consumed; diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index 22aec8f204d..cd1d84f235e 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -3,6 +3,7 @@ using DCL.Utilities.Extensions; using DCL.Utility.Types; using System; +using System.Diagnostics; using System.IO; using System.Threading; @@ -12,6 +13,12 @@ public static class DeepLinkSentinel { private static readonly TimeSpan CHECK_IN_PERIOD = TimeSpan.FromMilliseconds(200); + // A signin deep link is not consumed unless this instance is the one logging in, so concurrent idle + // Explorer instances don't steal it from each other via the shared bridge file. An unclaimed signin + // is kept this long so a login starting shortly after can still pick it up, then dropped: without + // this cap the deferred file would be re-read on every check-in forever. + private static readonly TimeSpan DEFERRED_SIGNIN_LIFETIME = TimeSpan.FromSeconds(300); + #if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || PLATFORM_STANDALONE_WIN // path for: C:\Users\\AppData\Local\DecentralandLauncherLight\ private static readonly string DEEP_LINK_BRIDGE_PATH = @@ -35,18 +42,26 @@ public static class DeepLinkSentinel /// public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandle handle, CancellationToken token) { + // Measures how long the current file has sat deferred (a signin nobody is logging in for yet), + // so it can be dropped once DEFERRED_SIGNIN_LIFETIME elapses instead of re-read indefinitely. + var deferralTimer = new Stopwatch(); + while (token.IsCancellationRequested == false) { bool cancelled = await UniTask.Delay(CHECK_IN_PERIOD, cancellationToken: token).SuppressCancellationThrow(); if (cancelled) continue; // File.Exists method is lightweight and can be used in this loop - if (!File.Exists(DEEP_LINK_BRIDGE_PATH)) continue; + if (!File.Exists(DEEP_LINK_BRIDGE_PATH)) + { + deferralTimer.Reset(); + continue; + } Result contentResult = await File.ReadAllTextAsync(DEEP_LINK_BRIDGE_PATH, token)!.SuppressToResultAsync(ReportCategory.RUNTIME_DEEPLINKS); // Transient IO read failure: leave the file for the next check-in. - if (contentResult.Success == false) continue; + if (!contentResult.Success) continue; // Parse before deleting: a corrupt file is dropped, a valid one is handled. Result deepLinkCreateResult = DeepLink.FromJson(contentResult.Value); @@ -55,10 +70,31 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl { ReportHub.LogError(ReportCategory.RUNTIME_DEEPLINKS, $"Cannot deserialize deeplink content: {deepLinkCreateResult.ErrorMessage}"); TryDeleteBridgeFile(); + deferralTimer.Reset(); + continue; + } + + DeepLinkHandleResult result = handle.HandleDeepLink(deepLinkCreateResult.Value); + + if (result == DeepLinkHandleResult.Deferred) + { + // Keep the file so the instance that is logging in claims the signin, instead of this + // idle one deleting it; a login starting within the grace period can still pick it up. + if (!deferralTimer.IsRunning) + deferralTimer.Restart(); + + if (deferralTimer.Elapsed < DEFERRED_SIGNIN_LIFETIME) + continue; + + ReportHub.LogWarning(ReportCategory.RUNTIME_DEEPLINKS, $"no login claimed the signin deeplink within {DEFERRED_SIGNIN_LIFETIME.TotalSeconds:0}s, dropping it: {deepLinkCreateResult.Value}"); + TryDeleteBridgeFile(); + deferralTimer.Reset(); continue; } - switch (handle.HandleDeepLink(deepLinkCreateResult.Value)) + deferralTimer.Reset(); + + switch (result) { case DeepLinkHandleResult.Consumed: ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"successfully handled deeplink: {deepLinkCreateResult.Value}"); diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index 3132cc17bcb..058506a6c40 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -33,6 +33,7 @@ public class DappDeepLinkAuthenticator : IWeb3Authenticator private readonly IWeb3AccountFactory web3AccountFactory; private readonly IWebRequestController webRequestController; private readonly ReactiveProperty deeplinkSigninIdentityId; + private readonly ReactiveProperty loginAwaitingSignin; private readonly int? identityExpirationDuration; private readonly URLBuilder urlBuilder = new (); @@ -43,6 +44,7 @@ public DappDeepLinkAuthenticator( IWeb3AccountFactory web3AccountFactory, IWebRequestController webRequestController, ReactiveProperty deeplinkSigninIdentityId, + ReactiveProperty loginAwaitingSignin, int? identityExpirationDuration = null) { this.webBrowser = webBrowser; @@ -51,6 +53,7 @@ public DappDeepLinkAuthenticator( this.web3AccountFactory = web3AccountFactory; this.webRequestController = webRequestController; this.deeplinkSigninIdentityId = deeplinkSigninIdentityId; + this.loginAwaitingSignin = loginAwaitingSignin; this.identityExpirationDuration = identityExpirationDuration; } @@ -119,6 +122,11 @@ private async UniTask WaitForSigninAsync(CancellationToken ct) { var completionSource = new UniTaskCompletionSource(); + // Signal the deep-link pipeline that this instance is now waiting: only then may it hand a + // signin over (and delete the shared bridge file). This is what keeps a concurrent idle Explorer + // instance from stealing the signin. It stays raised for the whole wait window below. + loginAwaitingSignin.Value = true; + using var subscription = deeplinkSigninIdentityId.UseCurrentValueAndSubscribeToUpdate(completionSource, static (identityId, completion) => { @@ -129,8 +137,9 @@ private async UniTask WaitForSigninAsync(CancellationToken ct) try { return await completionSource.Task.Timeout(TimeSpan.FromSeconds(DEEPLINK_TIMEOUT_SECONDS), DelayType.Realtime).AttachExternalCancellation(ct); } finally { - // Consume the id on success, timeout and cancellation alike, so a later - // login attempt never resolves against a signin delivered for this one. + // Stop accepting signins and consume the id on success, timeout and cancellation alike, so a + // later login attempt never resolves against a signin delivered for this one. + loginAwaitingSignin.Value = false; deeplinkSigninIdentityId.Value = null; } } From b5bb6faf34a5a351f12a9abf28470da4e7fedaad Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Wed, 8 Jul 2026 15:12:11 -0300 Subject: [PATCH 28/54] add bridge-only param on editor --- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index 058506a6c40..a11fb4f6bd4 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -82,6 +82,12 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio await UniTask.SwitchToMainThread(ct); var url = $"{signatureWebAppUrl}/{createRequestResponse.requestId}?loginMethod={payload.Method}&flow=deeplink"; + #if UNITY_EDITOR + // Tell the auth website to deliver the signin only by writing the bridge file, + // not by launching a new Explorer instance. + // Omitting this would spawn a build instance while the editor sits waiting for the signin. + url += "&bridge-only"; + #endif webBrowser.OpenUrlMainThreadOnly(url); From 4c39b26d40eb4c24cb8e8b0e03c2ba56a851d98a Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Wed, 8 Jul 2026 15:33:14 -0300 Subject: [PATCH 29/54] comments updating --- .../DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs | 4 +--- .../DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs | 5 +---- Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 3 +-- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 4 +--- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index 20805858242..0b98d4c4626 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -51,9 +51,7 @@ public class BootstrapContainer : DCLGlobalContainer public ICompositeWeb3Provider? CompositeWeb3Provider { get; private set; } public ReactiveProperty DeeplinkSigninIdentityId { get; } = new (null); - // Raised by the deep-link login flow while it waits for a signin; the deep-link pipeline only - // consumes a signin (and deletes the shared bridge file) while this is true, so a concurrent idle - // Explorer instance doesn't steal the signin from the instance that is actually logging in. + // True while this instance's login flow is actively waiting for a signin deep link. public ReactiveProperty DeeplinkLoginAwaitingSignin { get; } = new (false); public AnalyticsContainer Analytics { get; private set; } public DebugSettings.DebugSettings DebugSettings { get; private set; } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index 7859cdcd1eb..d38fc2f0f93 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -39,10 +39,7 @@ public DeepLinkHandleResult HandleDeepLink(DeepLink deeplink) if (!string.IsNullOrEmpty(signin)) { - // Only a login flow actively awaiting a signin may consume it. The bridge file is a single - // shared handoff point, so with several Explorer instances open an idle one would otherwise - // read and delete the file first, "stealing" the signin from the instance that is actually - // logging in. Leaving it in place lets the awaiting instance claim it instead. + // Guard: a signin may only be consumed by the instance that is actively awaiting one. if (!loginAwaitingSignin.Value) return DeepLinkHandleResult.Deferred; diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index cd1d84f235e..878f260b33b 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -78,8 +78,7 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl if (result == DeepLinkHandleResult.Deferred) { - // Keep the file so the instance that is logging in claims the signin, instead of this - // idle one deleting it; a login starting within the grace period can still pick it up. + // Leave the file in place: the login flow that claims it will delete it on consumption. if (!deferralTimer.IsRunning) deferralTimer.Restart(); diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index a11fb4f6bd4..ea5691cb790 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -128,9 +128,7 @@ private async UniTask WaitForSigninAsync(CancellationToken ct) { var completionSource = new UniTaskCompletionSource(); - // Signal the deep-link pipeline that this instance is now waiting: only then may it hand a - // signin over (and delete the shared bridge file). This is what keeps a concurrent idle Explorer - // instance from stealing the signin. It stays raised for the whole wait window below. + // Signals readiness: the deeplink pipeline only hands a signin over while this is true. loginAwaitingSignin.Value = true; using var subscription = deeplinkSigninIdentityId.UseCurrentValueAndSubscribeToUpdate(completionSource, From 04841c851c869eef97fdc1b683d9a2ad3ad8b245 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Thu, 9 Jul 2026 10:50:24 -0300 Subject: [PATCH 30/54] fix comments --- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index ea5691cb790..f7c285770f3 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -83,16 +83,14 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio var url = $"{signatureWebAppUrl}/{createRequestResponse.requestId}?loginMethod={payload.Method}&flow=deeplink"; #if UNITY_EDITOR - // Tell the auth website to deliver the signin only by writing the bridge file, - // not by launching a new Explorer instance. - // Omitting this would spawn a build instance while the editor sits waiting for the signin. + // Without this flag the auth website also launches a standalone Explorer build, + // which would steal the signin from the editor. url += "&bridge-only"; #endif webBrowser.OpenUrlMainThreadOnly(url); - // The browser builds and stores the AuthIdentity, then opens decentraland://?signin={identityId}, - // which is delivered here through the deep link pipeline. + // Resolves when the OS delivers the deep link that carries the identity string identityId = await WaitForSigninAsync(ct); return await FetchIdentityByIdAsync(identityId, ct); From 8c96b23cde0b465d717c6421a1dee6b69824ad6b Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Thu, 9 Jul 2026 10:55:31 -0300 Subject: [PATCH 31/54] fix merge errors --- .../Creations/CreationsDetailsPassportModuleController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Explorer/Assets/DCL/Passport/Modules/Creations/CreationsDetailsPassportModuleController.cs b/Explorer/Assets/DCL/Passport/Modules/Creations/CreationsDetailsPassportModuleController.cs index 6797c6a0bac..de758dd9e4e 100644 --- a/Explorer/Assets/DCL/Passport/Modules/Creations/CreationsDetailsPassportModuleController.cs +++ b/Explorer/Assets/DCL/Passport/Modules/Creations/CreationsDetailsPassportModuleController.cs @@ -33,7 +33,7 @@ public class CreationsDetailsPassportModuleController : IPassportModuleControlle private readonly NftTypeIconSO rarityBackgrounds; private readonly NFTColorsSO rarityColors; private readonly NftTypeIconSO categoryIcons; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly ImageControllerProvider imageControllerProvider; private readonly PassportErrorsController passportErrorsController; private readonly IObjectPool wearablesItemsPool; @@ -55,7 +55,7 @@ public CreationsDetailsPassportModuleController( NftTypeIconSO rarityBackgrounds, NFTColorsSO rarityColors, NftTypeIconSO categoryIcons, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, ImageControllerProvider imageControllerProvider, PassportErrorsController passportErrorsController) { @@ -254,7 +254,7 @@ private void SetupItemView(EquippedItemPassportFieldView itemView, MarketplaceCa itemView.OnSaleFlap.gameObject.SetActive(showBuy); RemoveNavigationListener(itemView); - UnityAction navigationListener = () => webBrowser.OpenUrl(marketplaceLink); + UnityAction navigationListener = () => webBrowser.OpenUrlMainThreadOnly(marketplaceLink); itemView.BuyButton.onClick.AddListener(navigationListener); itemView.ViewButton.onClick.AddListener(navigationListener); navigationListeners[itemView] = navigationListener; From 3035d28721e58d67501642d1fe8b5180e3f4d5a5 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Thu, 9 Jul 2026 12:32:32 -0300 Subject: [PATCH 32/54] wait for matching auth request id --- .../Global/AppArgs/AppArgsFlags.cs | 5 +++++ .../Global/Dynamic/BootstrapContainer.cs | 11 +++++----- .../Global/Dynamic/DynamicWorldContainer.cs | 2 +- .../DCL/RuntimeDeepLink/DeepLinkHandle.cs | 8 +++++--- .../DeepLinkHandleImplementation.cs | 13 +++++++----- .../Dapp/DappDeepLinkAuthenticator.cs | 20 ++++++++++--------- 6 files changed, 36 insertions(+), 23 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs index cfef6568a04..a36b9549cd6 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs @@ -34,6 +34,11 @@ public static class AppArgsFlags // The opaque identity id delivered by the auth website's signin deep link (decentraland://?signin={identityId}). public const string SIGNIN = "signin"; + // The auth request id the signin deep link belongs to, echoed back by the auth website. A login only + // consumes the signin when this matches the request id it minted, so signins are not crossed between + // concurrent logins or Explorer instances. + public const string AUTH_REQUEST_ID = "authRequestId"; + public const string FORCED_EMOTES = "self-force-emotes"; public const string SELF_PREVIEW_EMOTES = "self-preview-emotes"; public const string SELF_PREVIEW_WEARABLES = "self-preview-wearables"; diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index 0b98d4c4626..d91265e1b1b 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -51,8 +51,9 @@ public class BootstrapContainer : DCLGlobalContainer public ICompositeWeb3Provider? CompositeWeb3Provider { get; private set; } public ReactiveProperty DeeplinkSigninIdentityId { get; } = new (null); - // True while this instance's login flow is actively waiting for a signin deep link. - public ReactiveProperty DeeplinkLoginAwaitingSignin { get; } = new (false); + // The auth request id this instance's login flow is waiting a signin deep link for (null when not + // logging in). The deep-link pipeline consumes a signin only for a link whose authRequestId matches it. + public ReactiveProperty DeeplinkLoginAwaitingSigninRequestId { get; } = new (null); public AnalyticsContainer Analytics { get; private set; } public DebugSettings.DebugSettings DebugSettings { get; private set; } public VolumeBus VolumeBus { get; private set; } @@ -134,7 +135,7 @@ await bootstrapContainer.InitializeContainerAsync deeplinkSigninIdentityId, - ReactiveProperty deeplinkLoginAwaitingSignin) + ReactiveProperty deeplinkLoginAwaitingSigninRequestId) { int? identityExpirationDuration = appArgs.TryGetValue(AppArgsFlags.IDENTITY_EXPIRATION_DURATION, out string? v) ? int.Parse(v!) @@ -215,7 +216,7 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies( web3AccountFactory, webRequestController, deeplinkSigninIdentityId, - deeplinkLoginAwaitingSignin, + deeplinkLoginAwaitingSigninRequestId, identityExpirationDuration ); diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs index b289ae7c0fc..11ea425e8de 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -450,7 +450,7 @@ await MapRendererContainer // Deep link listening stays alive in every mode so browser sign-in can complete; local scene // development only opts out of navigation routing (teleports would break the scene under test). var deepLinkHandleImplementation = new DeepLinkHandle(dynamicWorldParams.StartParcel, chatContainer.ChatTeleporter, ct, communitiesDataService, bootstrapContainer.DeeplinkSigninIdentityId, - bootstrapContainer.DeeplinkLoginAwaitingSignin, routeNavigationDeepLinks: !appArgs.HasFlag(AppArgsFlags.LOCAL_SCENE)); + bootstrapContainer.DeeplinkLoginAwaitingSigninRequestId, routeNavigationDeepLinks: !appArgs.HasFlag(AppArgsFlags.LOCAL_SCENE)); deepLinkHandleImplementation.StartListenForDeepLinksAsync(ct).Forget(); diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs index 926c7afbb39..03009ee8a32 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs @@ -6,9 +6,11 @@ public enum DeepLinkHandleResult NoMatches, /// - /// A signin deep link arrived while no login flow was waiting for it. It is left untouched so the - /// instance actually logging in can claim it, rather than a concurrent idle Explorer instance - /// consuming and deleting the shared bridge file first. Kept until claimed (bounded by a timeout). + /// A signin deep link arrived that this instance must not consume: either no login here is waiting + /// for one, or its authRequestId does not match the request this login minted. It is left untouched + /// so the instance it was minted for can claim it from the shared bridge file, rather than a + /// concurrent or idle Explorer instance consuming and deleting it first. Kept until claimed by the + /// matching login (bounded by a timeout). /// Deferred, } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index d38fc2f0f93..cdb9216daef 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -18,18 +18,18 @@ public class DeepLinkHandle : IDeepLinkHandle private readonly CancellationToken token; private readonly CommunityDataService communityDataService; private readonly ReactiveProperty deeplinkSigninIdentityId; - private readonly IReadonlyReactiveProperty loginAwaitingSignin; + private readonly IReadonlyReactiveProperty loginAwaitingSigninRequestId; private readonly bool routeNavigationDeepLinks; public DeepLinkHandle(StartParcel startParcel, ChatTeleporter chatTeleporter, CancellationToken token, CommunityDataService communityDataService, ReactiveProperty deeplinkSigninIdentityId, - IReadonlyReactiveProperty loginAwaitingSignin, bool routeNavigationDeepLinks) + IReadonlyReactiveProperty loginAwaitingSigninRequestId, bool routeNavigationDeepLinks) { this.startParcel = startParcel; this.chatTeleporter = chatTeleporter; this.token = token; this.communityDataService = communityDataService; this.deeplinkSigninIdentityId = deeplinkSigninIdentityId; - this.loginAwaitingSignin = loginAwaitingSignin; + this.loginAwaitingSigninRequestId = loginAwaitingSigninRequestId; this.routeNavigationDeepLinks = routeNavigationDeepLinks; } @@ -39,8 +39,11 @@ public DeepLinkHandleResult HandleDeepLink(DeepLink deeplink) if (!string.IsNullOrEmpty(signin)) { - // Guard: a signin may only be consumed by the instance that is actively awaiting one. - if (!loginAwaitingSignin.Value) + string? awaitedRequestId = loginAwaitingSigninRequestId.Value; + + // Guard: only consume a signin while a login here is waiting for one, and only if the link + // was minted for that login. + if (string.IsNullOrEmpty(awaitedRequestId) || deeplink.ValueOf(AppArgsFlags.AUTH_REQUEST_ID) != awaitedRequestId) return DeepLinkHandleResult.Deferred; // The id persists in the property until it is overwritten or cleared. diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index f7c285770f3..850271d7a84 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -33,7 +33,7 @@ public class DappDeepLinkAuthenticator : IWeb3Authenticator private readonly IWeb3AccountFactory web3AccountFactory; private readonly IWebRequestController webRequestController; private readonly ReactiveProperty deeplinkSigninIdentityId; - private readonly ReactiveProperty loginAwaitingSignin; + private readonly ReactiveProperty loginAwaitingSigninRequestId; private readonly int? identityExpirationDuration; private readonly URLBuilder urlBuilder = new (); @@ -44,7 +44,7 @@ public DappDeepLinkAuthenticator( IWeb3AccountFactory web3AccountFactory, IWebRequestController webRequestController, ReactiveProperty deeplinkSigninIdentityId, - ReactiveProperty loginAwaitingSignin, + ReactiveProperty loginAwaitingSigninRequestId, int? identityExpirationDuration = null) { this.webBrowser = webBrowser; @@ -53,7 +53,7 @@ public DappDeepLinkAuthenticator( this.web3AccountFactory = web3AccountFactory; this.webRequestController = webRequestController; this.deeplinkSigninIdentityId = deeplinkSigninIdentityId; - this.loginAwaitingSignin = loginAwaitingSignin; + this.loginAwaitingSigninRequestId = loginAwaitingSigninRequestId; this.identityExpirationDuration = identityExpirationDuration; } @@ -90,8 +90,9 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio webBrowser.OpenUrlMainThreadOnly(url); - // Resolves when the OS delivers the deep link that carries the identity - string identityId = await WaitForSigninAsync(ct); + // Resolves when the OS delivers the deep link that carries the identity. The request id lets the + // pipeline hand back only the signin minted for this attempt (matched against the link's authRequestId). + string identityId = await WaitForSigninAsync(createRequestResponse.requestId, ct); return await FetchIdentityByIdAsync(identityId, ct); } @@ -122,12 +123,13 @@ private async UniTask CreateSigninRequestAsync(string /// /// Awaits the first non-empty identityId, starting from the currently stored one, and consumes it. /// - private async UniTask WaitForSigninAsync(CancellationToken ct) + private async UniTask WaitForSigninAsync(string requestId, CancellationToken ct) { var completionSource = new UniTaskCompletionSource(); - // Signals readiness: the deeplink pipeline only hands a signin over while this is true. - loginAwaitingSignin.Value = true; + // Publishes the request id awaited: the deeplink pipeline only hands a signin over while this is + // set, and only for a link whose authRequestId matches it. + loginAwaitingSigninRequestId.Value = requestId; using var subscription = deeplinkSigninIdentityId.UseCurrentValueAndSubscribeToUpdate(completionSource, static (identityId, completion) => @@ -141,7 +143,7 @@ private async UniTask WaitForSigninAsync(CancellationToken ct) { // Stop accepting signins and consume the id on success, timeout and cancellation alike, so a // later login attempt never resolves against a signin delivered for this one. - loginAwaitingSignin.Value = false; + loginAwaitingSigninRequestId.Value = null; deeplinkSigninIdentityId.Value = null; } } From db0dc5bab75c751ad00ee8fe1c093c49869e6cf1 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Thu, 9 Jul 2026 12:57:22 -0300 Subject: [PATCH 33/54] reduce compilation warnings --- ...entityVerificationDappDeepLinkAuthState.cs | 2 +- .../DCL/Events/EventDetailPanelController.cs | 23 ------------------- .../Assets/DCL/Navmap/SatelliteController.cs | 2 -- .../PluginSystem/Global/ExplorePanelPlugin.cs | 4 ---- .../DCL/RuntimeDeepLink/DeepLinkHandle.cs | 8 +++---- .../DeepLinkHandleImplementation.cs | 8 +++---- .../DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 6 ++--- .../Dapp/DappDeepLinkAuthenticator.cs | 5 +++- .../Dapp/DappWeb3EthereumApi.cs | 1 + 9 files changed, 17 insertions(+), 42 deletions(-) diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs index f3915e8591d..81ca9d02e00 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationDappDeepLinkAuthState.cs @@ -62,7 +62,7 @@ public override void Exit() Web3SignatureException ex => new SpanErrorInfo("Web3 signature validation failed during deep-link authentication", ex), DeeplinkSigninRetrievalException ex => new SpanErrorInfo($"Signin identity retrieval failed: {ex.Reason}", ex), Web3Exception ex => new SpanErrorInfo("Connection error during deep-link authentication flow", ex), - Exception ex => new SpanErrorInfo("Unexpected error during deep-link authentication flow", ex), + { } ex => new SpanErrorInfo("Unexpected error during deep-link authentication flow", ex), }; if (loginException is not OperationCanceledException) diff --git a/Explorer/Assets/DCL/Events/EventDetailPanelController.cs b/Explorer/Assets/DCL/Events/EventDetailPanelController.cs index bc9235ff2f8..d32efadc3f8 100644 --- a/Explorer/Assets/DCL/Events/EventDetailPanelController.cs +++ b/Explorer/Assets/DCL/Events/EventDetailPanelController.cs @@ -1,32 +1,16 @@ using Cysharp.Threading.Tasks; using DCL.Events; using DCL.EventsApi; -using DCL.NotificationsBus; -using DCL.NotificationsBus.NotificationTypes; -using DCL.UI; -using DCL.Utilities.Extensions; -using DCL.WebRequests; using MVC; using System; using System.Threading; -using CommunicationData.URLHelpers; -using DCL.Browser; -using DCL.Clipboard; -using DCL.CommunicationData.URLHelpers; -using UnityEngine; using Utility; namespace DCL.Communities.EventInfo { public class EventDetailPanelController : ControllerBase { - private const string LINK_COPIED_MESSAGE = "Link copied to clipboard!"; - private const string INTERESTED_CHANGED_ERROR_MESSAGE = "There was an error changing your interest on the event. Please try again."; - private readonly EventCardActionsController eventCardActionsController; - private readonly ISystemClipboard clipboard; - private readonly UnityAppWebBrowser webBrowser; - private readonly HttpEventsApiService eventsApiService; public override CanvasOrdering.SortingLayer Layer => CanvasOrdering.SortingLayer.POPUP; @@ -35,19 +19,12 @@ public class EventDetailPanelController : ControllerBase(); var eventInfoViewFactory = EventDetailPanelController.CreateLazily(eventDetailPanelViewAsset, null); eventDetailPanelController = new EventDetailPanelController(eventInfoViewFactory, - webRequestController, - clipboard, - webBrowser, - eventsApiService, eventsThumbnailLoader, eventCardActionsController); mvcManager.RegisterController(eventDetailPanelController); diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs index 03009ee8a32..4cabc14fbdd 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandle.cs @@ -2,8 +2,8 @@ namespace DCL.RuntimeDeepLink { public enum DeepLinkHandleResult { - Consumed, - NoMatches, + CONSUMED, + NO_MATCHES, /// /// A signin deep link arrived that this instance must not consume: either no login here is waiting @@ -12,7 +12,7 @@ public enum DeepLinkHandleResult /// concurrent or idle Explorer instance consuming and deleting it first. Kept until claimed by the /// matching login (bounded by a timeout). /// - Deferred, + DEFERRED, } public interface IDeepLinkHandle @@ -27,7 +27,7 @@ private Null() { } public DeepLinkHandleResult HandleDeepLink(DeepLink deeplink) => - DeepLinkHandleResult.Consumed; + DeepLinkHandleResult.CONSUMED; } } } diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs index cdb9216daef..bff5fda641b 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkHandleImplementation.cs @@ -44,17 +44,17 @@ public DeepLinkHandleResult HandleDeepLink(DeepLink deeplink) // Guard: only consume a signin while a login here is waiting for one, and only if the link // was minted for that login. if (string.IsNullOrEmpty(awaitedRequestId) || deeplink.ValueOf(AppArgsFlags.AUTH_REQUEST_ID) != awaitedRequestId) - return DeepLinkHandleResult.Deferred; + return DeepLinkHandleResult.DEFERRED; // The id persists in the property until it is overwritten or cleared. deeplinkSigninIdentityId.Value = signin; - return DeepLinkHandleResult.Consumed; + return DeepLinkHandleResult.CONSUMED; } if (!routeNavigationDeepLinks) { ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"navigation deep link routing is disabled, dropping: {deeplink}"); - return DeepLinkHandleResult.Consumed; + return DeepLinkHandleResult.CONSUMED; } Vector2Int? position = PositionFrom(deeplink); @@ -90,7 +90,7 @@ public DeepLinkHandleResult HandleDeepLink(DeepLink deeplink) handled = true; } - return handled ? DeepLinkHandleResult.Consumed : DeepLinkHandleResult.NoMatches; + return handled ? DeepLinkHandleResult.CONSUMED : DeepLinkHandleResult.NO_MATCHES; } private static URLDomain? RealmFrom(DeepLink deepLink) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index 878f260b33b..32e51fe5413 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -76,7 +76,7 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl DeepLinkHandleResult result = handle.HandleDeepLink(deepLinkCreateResult.Value); - if (result == DeepLinkHandleResult.Deferred) + if (result == DeepLinkHandleResult.DEFERRED) { // Leave the file in place: the login flow that claims it will delete it on consumption. if (!deferralTimer.IsRunning) @@ -95,10 +95,10 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl switch (result) { - case DeepLinkHandleResult.Consumed: + case DeepLinkHandleResult.CONSUMED: ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"successfully handled deeplink: {deepLinkCreateResult.Value}"); break; - case DeepLinkHandleResult.NoMatches: + case DeepLinkHandleResult.NO_MATCHES: ReportHub.LogWarning(ReportCategory.RUNTIME_DEEPLINKS, $"found no actionable content in deeplink: {deepLinkCreateResult.Value}"); break; } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index 850271d7a84..6f113767acb 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -202,12 +202,13 @@ private async UniTask FetchIdentityByIdAsync(string identi private static string CreateEphemeralMessage(IWeb3Account ephemeralAccount, DateTime expiration) => $"Decentraland Login\nEphemeral address: {ephemeralAccount.Address.OriginalFormat}\nExpiration: {expiration:yyyy-MM-ddTHH:mm:ss.fffZ}"; + // Field names mirror the auth server's JSON payloads verbatim, so they intentionally break the naming rules. + // ReSharper disable InconsistentNaming [Serializable] [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] private struct SigninRequestDto { public string method; - // ReSharper disable once InconsistentNaming public object[] @params; } @@ -240,5 +241,7 @@ public struct EphemeralIdentityDto public string publicKey; } } + + // ReSharper restore InconsistentNaming } } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs index 31c0ef22019..500f3f4fe3f 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs @@ -137,6 +137,7 @@ public async UniTask SendAsync(EthApiRequest request, Web3Reques /// ). The production wallet flow is . /// /// Login payload containing the authentication method + /// Token that cancels the login flow /// private async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) { From 3533102dd67f78ce6aedb2939e3cb5abad36f260 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:28:01 -0300 Subject: [PATCH 34/54] Update Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index 6f113767acb..218fb8eae36 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -99,7 +99,9 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio /// /// Mints a sign-in requestId via POST {authApiUrl}/requests. - /// The browser later recovers the request by that id to drive the wallet signature. + /// + /// Mints a sign-in requestId via POST {authApiUrl}/requests. + /// /// private async UniTask CreateSigninRequestAsync(string ephemeralMessage, CancellationToken ct) { From dc68e6e9eb9516ed0e77cb69102ee98f4dbc7926 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:28:12 -0300 Subject: [PATCH 35/54] Update Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index 32e51fe5413..17701f74235 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -78,7 +78,7 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl if (result == DeepLinkHandleResult.DEFERRED) { - // Leave the file in place: the login flow that claims it will delete it on consumption. + // Leave the file in place so the awaiting login can claim it. if (!deferralTimer.IsRunning) deferralTimer.Restart(); From 42a670dbb4bc4931c847a4428505a1334fdffd60 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:28:35 -0300 Subject: [PATCH 36/54] Update Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- .../DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index d91265e1b1b..58d8ec50d50 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -53,6 +53,7 @@ public class BootstrapContainer : DCLGlobalContainer // The auth request id this instance's login flow is waiting a signin deep link for (null when not // logging in). The deep-link pipeline consumes a signin only for a link whose authRequestId matches it. + // The auth request id this instance's login flow is waiting a signin deep link for (null when not logging in). public ReactiveProperty DeeplinkLoginAwaitingSigninRequestId { get; } = new (null); public AnalyticsContainer Analytics { get; private set; } public DebugSettings.DebugSettings DebugSettings { get; private set; } From d30b0baeba0b1904faa56404008f926a5fb28b4a Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:28:52 -0300 Subject: [PATCH 37/54] Update Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- .../Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs index a36b9549cd6..9e45c8f9528 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs @@ -37,6 +37,7 @@ public static class AppArgsFlags // The auth request id the signin deep link belongs to, echoed back by the auth website. A login only // consumes the signin when this matches the request id it minted, so signins are not crossed between // concurrent logins or Explorer instances. + // The auth request id parameter echoed in the signin deep link, used to match a link to the login that minted it. public const string AUTH_REQUEST_ID = "authRequestId"; public const string FORCED_EMOTES = "self-force-emotes"; From 2978f3c09fdf6aed7e00a2737f65c0a564525609 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:29:04 -0300 Subject: [PATCH 38/54] Update Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index 218fb8eae36..8a34fed97e9 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -131,6 +131,7 @@ private async UniTask WaitForSigninAsync(string requestId, CancellationT // Publishes the request id awaited: the deeplink pipeline only hands a signin over while this is // set, and only for a link whose authRequestId matches it. + // Publishes the request id awaited by the pipeline. loginAwaitingSigninRequestId.Value = requestId; using var subscription = deeplinkSigninIdentityId.UseCurrentValueAndSubscribeToUpdate(completionSource, From 20de70b6eb60d1e897ad1e7348ac5e706bfc378e Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:59:18 -0300 Subject: [PATCH 39/54] Update Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index 8a34fed97e9..7cadf1ebb81 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -90,8 +90,7 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio webBrowser.OpenUrlMainThreadOnly(url); - // Resolves when the OS delivers the deep link that carries the identity. The request id lets the - // pipeline hand back only the signin minted for this attempt (matched against the link's authRequestId). + // Resolves when the OS delivers the deep link that carries the identity id. string identityId = await WaitForSigninAsync(createRequestResponse.requestId, ct); return await FetchIdentityByIdAsync(identityId, ct); From eca858726e22191863cc77e0545ed05dea6dfe6f Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:59:38 -0300 Subject: [PATCH 40/54] Update Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index 7cadf1ebb81..04826325a76 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -98,9 +98,6 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio /// /// Mints a sign-in requestId via POST {authApiUrl}/requests. - /// - /// Mints a sign-in requestId via POST {authApiUrl}/requests. - /// /// private async UniTask CreateSigninRequestAsync(string ephemeralMessage, CancellationToken ct) { From 65f8a23b535bd6a3ec0b985d9fa1fc911aaac2ef Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:59:48 -0300 Subject: [PATCH 41/54] Update Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- .../Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs index 9e45c8f9528..36421b2b1ef 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs @@ -34,9 +34,6 @@ public static class AppArgsFlags // The opaque identity id delivered by the auth website's signin deep link (decentraland://?signin={identityId}). public const string SIGNIN = "signin"; - // The auth request id the signin deep link belongs to, echoed back by the auth website. A login only - // consumes the signin when this matches the request id it minted, so signins are not crossed between - // concurrent logins or Explorer instances. // The auth request id parameter echoed in the signin deep link, used to match a link to the login that minted it. public const string AUTH_REQUEST_ID = "authRequestId"; From 85b7967544c589d4249c1a20269cc3323290922f Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:59:57 -0300 Subject: [PATCH 42/54] Update Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- .../DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index 58d8ec50d50..3cc93512a3f 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -51,8 +51,6 @@ public class BootstrapContainer : DCLGlobalContainer public ICompositeWeb3Provider? CompositeWeb3Provider { get; private set; } public ReactiveProperty DeeplinkSigninIdentityId { get; } = new (null); - // The auth request id this instance's login flow is waiting a signin deep link for (null when not - // logging in). The deep-link pipeline consumes a signin only for a link whose authRequestId matches it. // The auth request id this instance's login flow is waiting a signin deep link for (null when not logging in). public ReactiveProperty DeeplinkLoginAwaitingSigninRequestId { get; } = new (null); public AnalyticsContainer Analytics { get; private set; } From 6786673166ed42e88e2c77475991f0bb79d4ef5c Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:00:08 -0300 Subject: [PATCH 43/54] Update Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index 04826325a76..b32af14e1aa 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -125,8 +125,6 @@ private async UniTask WaitForSigninAsync(string requestId, CancellationT { var completionSource = new UniTaskCompletionSource(); - // Publishes the request id awaited: the deeplink pipeline only hands a signin over while this is - // set, and only for a link whose authRequestId matches it. // Publishes the request id awaited by the pipeline. loginAwaitingSigninRequestId.Value = requestId; From dc79f08bff546f756c07c51975a0807c1fc1a800 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Mon, 13 Jul 2026 12:06:17 -0300 Subject: [PATCH 44/54] dont request auth id anymore --- .../Global/Dynamic/BootstrapContainer.cs | 3 +- .../Dapp/DappDeepLinkAuthenticator.cs | 83 +++---------------- 2 files changed, 13 insertions(+), 73 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs index 3cc93512a3f..b830f820ad2 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs @@ -215,8 +215,7 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies( web3AccountFactory, webRequestController, deeplinkSigninIdentityId, - deeplinkLoginAwaitingSigninRequestId, - identityExpirationDuration + deeplinkLoginAwaitingSigninRequestId ); var dappAuth = new DappWeb3EthereumApi( diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index b32af14e1aa..eaeefd29410 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -7,9 +7,7 @@ using DCL.Web3.Chains; using DCL.Web3.Identities; using DCL.WebRequests; -using JetBrains.Annotations; using Nethereum.Signer; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Globalization; @@ -18,13 +16,13 @@ namespace DCL.Web3.Authenticators { /// - /// Production wallet sign-in via browser and OS deep link: initiates a sign-in request on the auth server, - /// opens the browser for the user to sign with their wallet, then awaits the deep link identity id - /// and resolves the resulting identity from the server. + /// Production wallet sign-in via browser and OS deep link: generates a client-side auth request id, + /// opens the browser on the signature web app for the user to sign with their wallet, then awaits the + /// deep link that carries the resulting identity id (matched against that request id) and resolves the + /// identity from the auth server. /// public class DappDeepLinkAuthenticator : IWeb3Authenticator { - private const double IDENTITY_EXPIRATION_PERIOD_FALLBACK_IN_DAYS = 30; private const int DEEPLINK_TIMEOUT_SECONDS = 300; private readonly UnityAppWebBrowser webBrowser; @@ -34,7 +32,6 @@ public class DappDeepLinkAuthenticator : IWeb3Authenticator private readonly IWebRequestController webRequestController; private readonly ReactiveProperty deeplinkSigninIdentityId; private readonly ReactiveProperty loginAwaitingSigninRequestId; - private readonly int? identityExpirationDuration; private readonly URLBuilder urlBuilder = new (); public DappDeepLinkAuthenticator( @@ -44,8 +41,7 @@ public DappDeepLinkAuthenticator( IWeb3AccountFactory web3AccountFactory, IWebRequestController webRequestController, ReactiveProperty deeplinkSigninIdentityId, - ReactiveProperty loginAwaitingSigninRequestId, - int? identityExpirationDuration = null) + ReactiveProperty loginAwaitingSigninRequestId) { this.webBrowser = webBrowser; this.authApiUrl = authApiUrl; @@ -54,7 +50,6 @@ public DappDeepLinkAuthenticator( this.webRequestController = webRequestController; this.deeplinkSigninIdentityId = deeplinkSigninIdentityId; this.loginAwaitingSigninRequestId = loginAwaitingSigninRequestId; - this.identityExpirationDuration = identityExpirationDuration; } public void Dispose() { } @@ -65,59 +60,27 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio // so a stale or foreign deep link does not complete this login with another session's identity. deeplinkSigninIdentityId.Value = null; - // The ephemeral address is embedded in the signed message so the server can mint a well-formed request from it. - var ephemeralAccount = web3AccountFactory.CreateRandomAccount(); - - DateTime sessionExpiration = identityExpirationDuration != null - ? DateTime.UtcNow.AddSeconds(identityExpirationDuration.Value) - : DateTime.UtcNow.AddDays(IDENTITY_EXPIRATION_PERIOD_FALLBACK_IN_DAYS); - - string ephemeralMessage = CreateEphemeralMessage(ephemeralAccount, sessionExpiration); - - CreateRequestResponseDto createRequestResponse = await CreateSigninRequestAsync(ephemeralMessage, ct); - - if (string.IsNullOrEmpty(createRequestResponse.requestId)) - throw new Web3Exception("Cannot solve auth request id"); - await UniTask.SwitchToMainThread(ct); - var url = $"{signatureWebAppUrl}/{createRequestResponse.requestId}?loginMethod={payload.Method}&flow=deeplink"; + // Auth request ids are not requested to the auth server anymore but instead generated at client level, + // so it can later be validated through the flow + var authRequestId = Guid.NewGuid().ToString(); + + var url = $"{signatureWebAppUrl}/{authRequestId}?loginMethod={payload.Method}&flow=deeplink"; #if UNITY_EDITOR // Without this flag the auth website also launches a standalone Explorer build, // which would steal the signin from the editor. - url += "&bridge-only"; + url += "&bridgeOnly"; #endif webBrowser.OpenUrlMainThreadOnly(url); // Resolves when the OS delivers the deep link that carries the identity id. - string identityId = await WaitForSigninAsync(createRequestResponse.requestId, ct); + string identityId = await WaitForSigninAsync(authRequestId, ct); return await FetchIdentityByIdAsync(identityId, ct); } - /// - /// Mints a sign-in requestId via POST {authApiUrl}/requests. - /// - private async UniTask CreateSigninRequestAsync(string ephemeralMessage, CancellationToken ct) - { - urlBuilder.Clear(); - - urlBuilder.AppendDomain(URLDomain.FromString(authApiUrl)) - .AppendPath(new URLPath("requests")); - - var commonArguments = new CommonArguments(urlBuilder.Build(), RetryPolicy.Enforce()); - - string body = JsonConvert.SerializeObject(new SigninRequestDto - { - method = "dcl_personal_sign", - @params = new object[] { ephemeralMessage }, - }); - - return await webRequestController.PostAsync(commonArguments, GenericPostArguments.CreateJson(body), ct, ReportCategory.AUTHENTICATION) - .CreateFromNewtonsoftJsonAsync(); - } - /// /// Awaits the first non-empty identityId, starting from the currently stored one, and consumes it. /// @@ -196,27 +159,7 @@ private async UniTask FetchIdentityByIdAsync(string identi return new DecentralandIdentity(new Web3Address(signerAddress), ephemeralAccount, expiration, authChain, IWeb3Identity.Web3IdentitySource.Deeplink); } - private static string CreateEphemeralMessage(IWeb3Account ephemeralAccount, DateTime expiration) => - $"Decentraland Login\nEphemeral address: {ephemeralAccount.Address.OriginalFormat}\nExpiration: {expiration:yyyy-MM-ddTHH:mm:ss.fffZ}"; - // Field names mirror the auth server's JSON payloads verbatim, so they intentionally break the naming rules. - // ReSharper disable InconsistentNaming - [Serializable] - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] - private struct SigninRequestDto - { - public string method; - public object[] @params; - } - - [Serializable] - private struct CreateRequestResponseDto - { - public string requestId; - public string expiration; - public int code; - } - [Serializable] private struct IdentityAuthResponseDto { @@ -238,7 +181,5 @@ public struct EphemeralIdentityDto public string publicKey; } } - - // ReSharper restore InconsistentNaming } } From f3500ea71b07a5d383bdd86baf5d7a28784f1db4 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:45:17 -0300 Subject: [PATCH 45/54] Update Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index eaeefd29410..1610bd285e6 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -65,7 +65,8 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio // Auth request ids are not requested to the auth server anymore but instead generated at client level, // so it can later be validated through the flow var authRequestId = Guid.NewGuid().ToString(); - + // Client-generated id embedded in the browser URL; no server round-trip needed before opening the browser. + var authRequestId = Guid.NewGuid().ToString(); var url = $"{signatureWebAppUrl}/{authRequestId}?loginMethod={payload.Method}&flow=deeplink"; #if UNITY_EDITOR // Without this flag the auth website also launches a standalone Explorer build, From 8493198d901c3e02c45508023196fb9c10b25cc1 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Tue, 14 Jul 2026 10:43:48 -0300 Subject: [PATCH 46/54] fix compilation err --- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index 1610bd285e6..bd9e1ee7321 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -62,9 +62,6 @@ public async UniTask LoginAsync(LoginPayload payload, Cancellatio await UniTask.SwitchToMainThread(ct); - // Auth request ids are not requested to the auth server anymore but instead generated at client level, - // so it can later be validated through the flow - var authRequestId = Guid.NewGuid().ToString(); // Client-generated id embedded in the browser URL; no server round-trip needed before opening the browser. var authRequestId = Guid.NewGuid().ToString(); var url = $"{signatureWebAppUrl}/{authRequestId}?loginMethod={payload.Method}&flow=deeplink"; From 29a0dbb7a38b4b5a303edfacedf512b48eed182a Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:11:32 -0300 Subject: [PATCH 47/54] Update Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index 17701f74235..a427b5b7f99 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -14,9 +14,7 @@ public static class DeepLinkSentinel private static readonly TimeSpan CHECK_IN_PERIOD = TimeSpan.FromMilliseconds(200); // A signin deep link is not consumed unless this instance is the one logging in, so concurrent idle - // Explorer instances don't steal it from each other via the shared bridge file. An unclaimed signin - // is kept this long so a login starting shortly after can still pick it up, then dropped: without - // this cap the deferred file would be re-read on every check-in forever. + // Maximum time a deferred signin bridge file is retained on disk; without this cap it would be re-read on every check-in forever. private static readonly TimeSpan DEFERRED_SIGNIN_LIFETIME = TimeSpan.FromSeconds(300); #if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || PLATFORM_STANDALONE_WIN From 4df5a244bb89865db2f39283658d88154786937f Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:23:30 -0300 Subject: [PATCH 48/54] Update Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index a427b5b7f99..7d72ccd4e4c 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -13,7 +13,6 @@ public static class DeepLinkSentinel { private static readonly TimeSpan CHECK_IN_PERIOD = TimeSpan.FromMilliseconds(200); - // A signin deep link is not consumed unless this instance is the one logging in, so concurrent idle // Maximum time a deferred signin bridge file is retained on disk; without this cap it would be re-read on every check-in forever. private static readonly TimeSpan DEFERRED_SIGNIN_LIFETIME = TimeSpan.FromSeconds(300); From 427efd42d33330befeb46316e8e4b210d4082876 Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Thu, 16 Jul 2026 11:42:03 -0300 Subject: [PATCH 49/54] review fixes --- .../DCL/RuntimeDeepLink/DeepLinkSentinel.cs | 6 +-- .../Dapp/DappDeepLinkAuthenticator.cs | 53 +++++++++++-------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs index 7d72ccd4e4c..0ef1385ad1f 100644 --- a/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs +++ b/Explorer/Assets/DCL/RuntimeDeepLink/DeepLinkSentinel.cs @@ -82,7 +82,7 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl if (deferralTimer.Elapsed < DEFERRED_SIGNIN_LIFETIME) continue; - ReportHub.LogWarning(ReportCategory.RUNTIME_DEEPLINKS, $"no login claimed the signin deeplink within {DEFERRED_SIGNIN_LIFETIME.TotalSeconds:0}s, dropping it: {deepLinkCreateResult.Value}"); + ReportHub.LogWarning(ReportCategory.RUNTIME_DEEPLINKS, $"no login claimed the signin deeplink within {DEFERRED_SIGNIN_LIFETIME.TotalSeconds:0}s, dropping it"); TryDeleteBridgeFile(); deferralTimer.Reset(); continue; @@ -93,10 +93,10 @@ public static async UniTaskVoid StartListenForDeepLinksAsync(this IDeepLinkHandl switch (result) { case DeepLinkHandleResult.CONSUMED: - ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, $"successfully handled deeplink: {deepLinkCreateResult.Value}"); + ReportHub.Log(ReportCategory.RUNTIME_DEEPLINKS, "successfully handled deeplink"); break; case DeepLinkHandleResult.NO_MATCHES: - ReportHub.LogWarning(ReportCategory.RUNTIME_DEEPLINKS, $"found no actionable content in deeplink: {deepLinkCreateResult.Value}"); + ReportHub.LogWarning(ReportCategory.RUNTIME_DEEPLINKS, "found no actionable content in deeplink"); break; } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index bd9e1ee7321..e8d60302af0 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Globalization; using System.Threading; +using Utility.Multithreading; namespace DCL.Web3.Authenticators { @@ -33,6 +34,7 @@ public class DappDeepLinkAuthenticator : IWeb3Authenticator private readonly ReactiveProperty deeplinkSigninIdentityId; private readonly ReactiveProperty loginAwaitingSigninRequestId; private readonly URLBuilder urlBuilder = new (); + private readonly DCLSemaphoreSlim loginMutex = new (1, 1); public DappDeepLinkAuthenticator( UnityAppWebBrowser webBrowser, @@ -56,27 +58,34 @@ public void Dispose() { } public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) { - // A signin id stored before this attempt minted its request cannot belong to it: drop it - // so a stale or foreign deep link does not complete this login with another session's identity. - deeplinkSigninIdentityId.Value = null; + await loginMutex.WaitAsync(ct); - await UniTask.SwitchToMainThread(ct); + try + { + // A signin id stored before this attempt minted its request cannot belong to it: drop it + // so a stale or foreign deep link does not complete this login with another session's identity. + deeplinkSigninIdentityId.Value = null; + + await UniTask.SwitchToMainThread(ct); - // Client-generated id embedded in the browser URL; no server round-trip needed before opening the browser. - var authRequestId = Guid.NewGuid().ToString(); - var url = $"{signatureWebAppUrl}/{authRequestId}?loginMethod={payload.Method}&flow=deeplink"; - #if UNITY_EDITOR - // Without this flag the auth website also launches a standalone Explorer build, - // which would steal the signin from the editor. - url += "&bridgeOnly"; - #endif + // Client-generated id embedded in the browser URL; no server round-trip needed before opening the browser. + var authRequestId = Guid.NewGuid().ToString(); + var url = $"{signatureWebAppUrl}/{authRequestId}?loginMethod={payload.Method}&flow=deeplink"; +#if UNITY_EDITOR - webBrowser.OpenUrlMainThreadOnly(url); + // Without this flag the auth website also launches a standalone Explorer build, + // which would steal the signin from the editor. + url += "&bridgeOnly"; +#endif - // Resolves when the OS delivers the deep link that carries the identity id. - string identityId = await WaitForSigninAsync(authRequestId, ct); + webBrowser.OpenUrlMainThreadOnly(url); - return await FetchIdentityByIdAsync(identityId, ct); + // Resolves when the OS delivers the deep link that carries the identity id. + string identityId = await WaitForSigninAsync(authRequestId, ct); + + return await FetchIdentityByIdAsync(identityId, ct); + } + finally { loginMutex.Release(); } } /// @@ -118,12 +127,12 @@ private async UniTask FetchIdentityByIdAsync(string identi IdentityAuthResponseDto json = await webRequestController.GetAsync(commonArguments, ct, ReportCategory.AUTHENTICATION) .CreateFromNewtonsoftJsonAsync() .WithCustomExceptionAsync(e => e.ResponseCode switch - { - 404 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.NOT_FOUND, identityId), - 410 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.EXPIRED, identityId), - 403 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.IP_MISMATCH, identityId), - _ => e, - }); + { + 404 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.NOT_FOUND, identityId), + 410 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.EXPIRED, identityId), + 403 => new DeeplinkSigninRetrievalException(DeeplinkSigninRetrievalException.ErrorReason.IP_MISMATCH, identityId), + _ => e, + }); string? signerAddress = null; string? ephemeralPayload = null; From afbf784e7c052f91d461a74eb11df851edce853b Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Thu, 16 Jul 2026 11:45:20 -0300 Subject: [PATCH 50/54] delete iwebbrowser file --- Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs diff --git a/Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs b/Explorer/Assets/DCL/NetworkDefinitions/Browser/IWebBrowser.cs deleted file mode 100644 index e69de29bb2d..00000000000 From 2328a7f521a5a1418d4d7e2e50880f2d7ce3001e Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:10:45 -0300 Subject: [PATCH 51/54] Update Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com> Signed-off-by: Nicolas Lorusso <56365551+lorux0@users.noreply.github.com> --- .../Implementations/Dapp/DappDeepLinkAuthenticator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index e8d60302af0..bf06673b953 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -54,7 +54,10 @@ public DappDeepLinkAuthenticator( this.loginAwaitingSigninRequestId = loginAwaitingSigninRequestId; } - public void Dispose() { } + public void Dispose() + { + loginMutex.Dispose(); + } public async UniTask LoginAsync(LoginPayload payload, CancellationToken ct) { From 884fc77573e9162d253d6cae22d27814359e7fca Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Fri, 17 Jul 2026 11:02:13 -0300 Subject: [PATCH 52/54] fix web browser naming --- .../Tests/CreditPurchaseBuyHandlerShould.cs | 14 +++++++------- .../Purchase/UI/CreditPurchaseModalController.cs | 6 +++--- .../Passport/Modules/CreditPurchaseBuyHandler.cs | 10 +++++----- .../PluginSystem/Global/CreditPurchasePlugin.cs | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Explorer/Assets/DCL/MarketplaceCredits/Purchase/Tests/CreditPurchaseBuyHandlerShould.cs b/Explorer/Assets/DCL/MarketplaceCredits/Purchase/Tests/CreditPurchaseBuyHandlerShould.cs index 9b2eddcdc0f..4abd21651ab 100644 --- a/Explorer/Assets/DCL/MarketplaceCredits/Purchase/Tests/CreditPurchaseBuyHandlerShould.cs +++ b/Explorer/Assets/DCL/MarketplaceCredits/Purchase/Tests/CreditPurchaseBuyHandlerShould.cs @@ -19,14 +19,14 @@ public class CreditPurchaseBuyHandlerShould private IMVCManager mvcManager = null!; private MarketplaceShopAPIClient shopAPIClient = null!; - private IWebBrowser webBrowser = null!; + private UnityAppWebBrowser webBrowser = null!; [SetUp] public void SetUp() { mvcManager = Substitute.For(); shopAPIClient = Substitute.For(null, null); - webBrowser = Substitute.For(); + webBrowser = Substitute.For(); } private CreditPurchaseBuyHandler CreateHandler(bool isEnabled) => @@ -45,7 +45,7 @@ public async Task RedirectToWebWhenFeatureIsDisabled() await handler.HandleBuyClickAsync(ITEM_URN, MARKETPLACE_URL, CreateVisuals(), _ => { }, CancellationToken.None); // Assert - webBrowser.Received(1).OpenUrl(MARKETPLACE_URL); + webBrowser.Received(1).OpenUrlMainThreadOnly(MARKETPLACE_URL); await shopAPIClient.DidNotReceive().GetShopListingForItemAsync(Arg.Any(), Arg.Any(), Arg.Any()); } @@ -63,7 +63,7 @@ public async Task OpenPurchaseModalWhenListingIsCreditBuyable() // Assert await mvcManager.Received(1).ShowAsync(Arg.Any>(), Arg.Any()); - webBrowser.DidNotReceive().OpenUrl(Arg.Any()); + webBrowser.DidNotReceive().OpenUrlMainThreadOnly(Arg.Any()); } [Test] @@ -79,7 +79,7 @@ public async Task RedirectToWebWhenItemHasNoCreditListing() await handler.HandleBuyClickAsync(ITEM_URN, MARKETPLACE_URL, CreateVisuals(), _ => { }, CancellationToken.None); // Assert - webBrowser.Received(1).OpenUrl(MARKETPLACE_URL); + webBrowser.Received(1).OpenUrlMainThreadOnly(MARKETPLACE_URL); await mvcManager.DidNotReceive().ShowAsync(Arg.Any>(), Arg.Any()); } @@ -96,7 +96,7 @@ public async Task RedirectToWebWhenListingResolutionFails() await handler.HandleBuyClickAsync(ITEM_URN, MARKETPLACE_URL, CreateVisuals(), _ => { }, CancellationToken.None); // Assert - webBrowser.Received(1).OpenUrl(MARKETPLACE_URL); + webBrowser.Received(1).OpenUrlMainThreadOnly(MARKETPLACE_URL); } [Test] @@ -109,7 +109,7 @@ public async Task RedirectToWebWhenUrnIsNotParseable() await handler.HandleBuyClickAsync("urn:decentraland:off-chain:base-avatars:brown_pants", MARKETPLACE_URL, CreateVisuals(), _ => { }, CancellationToken.None); // Assert - webBrowser.Received(1).OpenUrl(MARKETPLACE_URL); + webBrowser.Received(1).OpenUrlMainThreadOnly(MARKETPLACE_URL); await shopAPIClient.DidNotReceive().GetShopListingForItemAsync(Arg.Any(), Arg.Any(), Arg.Any()); } diff --git a/Explorer/Assets/DCL/MarketplaceCredits/Purchase/UI/CreditPurchaseModalController.cs b/Explorer/Assets/DCL/MarketplaceCredits/Purchase/UI/CreditPurchaseModalController.cs index c46e53f5110..5618187b96d 100644 --- a/Explorer/Assets/DCL/MarketplaceCredits/Purchase/UI/CreditPurchaseModalController.cs +++ b/Explorer/Assets/DCL/MarketplaceCredits/Purchase/UI/CreditPurchaseModalController.cs @@ -25,7 +25,7 @@ private enum ModalState private readonly ICreditsPurchaseService purchaseService; private readonly MarketplaceCreditsAPIClient creditsAPIClient; private readonly IWeb3IdentityCache identityCache; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly Func openGetCreditsPanelAsync; private readonly CancellationTokenSource disposalCts = new (); @@ -40,7 +40,7 @@ public CreditPurchaseModalController( ICreditsPurchaseService purchaseService, MarketplaceCreditsAPIClient creditsAPIClient, IWeb3IdentityCache identityCache, - IWebBrowser webBrowser, + UnityAppWebBrowser webBrowser, Func openGetCreditsPanelAsync) : base(viewFactory) { @@ -175,7 +175,7 @@ private void OnGetCreditsClicked() private void OnOpenMarketplaceClicked() { if (!string.IsNullOrEmpty(inputData.FallbackMarketplaceUrl)) - webBrowser.OpenUrl(inputData.FallbackMarketplaceUrl); + webBrowser.OpenUrlMainThreadOnly(inputData.FallbackMarketplaceUrl); } private async UniTask PurchaseAsync(CancellationToken ct) diff --git a/Explorer/Assets/DCL/Passport/Modules/CreditPurchaseBuyHandler.cs b/Explorer/Assets/DCL/Passport/Modules/CreditPurchaseBuyHandler.cs index b4372260707..345a1cf0339 100644 --- a/Explorer/Assets/DCL/Passport/Modules/CreditPurchaseBuyHandler.cs +++ b/Explorer/Assets/DCL/Passport/Modules/CreditPurchaseBuyHandler.cs @@ -35,11 +35,11 @@ public ItemVisuals(string name, string rarityName, Sprite? thumbnail, Sprite? ra private readonly IMVCManager mvcManager; private readonly MarketplaceShopAPIClient shopAPIClient; - private readonly IWebBrowser webBrowser; + private readonly UnityAppWebBrowser webBrowser; private readonly bool isEnabled; private readonly Dictionary listingCache = new (); - public CreditPurchaseBuyHandler(IMVCManager mvcManager, MarketplaceShopAPIClient shopAPIClient, IWebBrowser webBrowser, bool isEnabled) + public CreditPurchaseBuyHandler(IMVCManager mvcManager, MarketplaceShopAPIClient shopAPIClient, UnityAppWebBrowser webBrowser, bool isEnabled) { this.mvcManager = mvcManager; this.shopAPIClient = shopAPIClient; @@ -78,7 +78,7 @@ public async UniTask HandleBuyClickAsync(string itemUrn, string marketplaceUrl, { if (!isEnabled) { - webBrowser.OpenUrl(marketplaceUrl); + webBrowser.OpenUrlMainThreadOnly(marketplaceUrl); return; } @@ -96,7 +96,7 @@ public async UniTask HandleBuyClickAsync(string itemUrn, string marketplaceUrl, catch (Exception e) { ReportHub.LogWarning(ReportCategory.CREDITS_PURCHASE, $"Listing resolution failed for {itemUrn}: {e.Message}"); - webBrowser.OpenUrl(marketplaceUrl); + webBrowser.OpenUrlMainThreadOnly(marketplaceUrl); return; } finally @@ -109,7 +109,7 @@ public async UniTask HandleBuyClickAsync(string itemUrn, string marketplaceUrl, if (listing == null) { - webBrowser.OpenUrl(marketplaceUrl); + webBrowser.OpenUrlMainThreadOnly(marketplaceUrl); return; } diff --git a/Explorer/Assets/DCL/PluginSystem/Global/CreditPurchasePlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/CreditPurchasePlugin.cs index 291868b1f65..44a71c1ea47 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/CreditPurchasePlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/CreditPurchasePlugin.cs @@ -21,7 +21,7 @@ public class CreditPurchasePlugin : IDCLGlobalPlugin Date: Fri, 17 Jul 2026 11:58:10 -0300 Subject: [PATCH 53/54] fix credit purchase tests due web browser --- .../Tests/CreditPurchaseBuyHandlerShould.cs | 32 +++++++++++++++---- .../Browser/UnityAppWebBrowser.cs | 4 +-- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Explorer/Assets/DCL/MarketplaceCredits/Purchase/Tests/CreditPurchaseBuyHandlerShould.cs b/Explorer/Assets/DCL/MarketplaceCredits/Purchase/Tests/CreditPurchaseBuyHandlerShould.cs index 4abd21651ab..128720ec262 100644 --- a/Explorer/Assets/DCL/MarketplaceCredits/Purchase/Tests/CreditPurchaseBuyHandlerShould.cs +++ b/Explorer/Assets/DCL/MarketplaceCredits/Purchase/Tests/CreditPurchaseBuyHandlerShould.cs @@ -1,7 +1,9 @@ using Cysharp.Threading.Tasks; using DCL.Browser; using DCL.MarketplaceCredits.Purchase.UI; +using DCL.Multiplayer.Connections.DecentralandUrls; using DCL.Passport.Modules; +using JetBrains.Annotations; using MVC; using NSubstitute; using NUnit.Framework; @@ -19,14 +21,14 @@ public class CreditPurchaseBuyHandlerShould private IMVCManager mvcManager = null!; private MarketplaceShopAPIClient shopAPIClient = null!; - private UnityAppWebBrowser webBrowser = null!; + private MockWebBrowser webBrowser = null!; [SetUp] public void SetUp() { mvcManager = Substitute.For(); shopAPIClient = Substitute.For(null, null); - webBrowser = Substitute.For(); + webBrowser = new MockWebBrowser(); } private CreditPurchaseBuyHandler CreateHandler(bool isEnabled) => @@ -45,7 +47,7 @@ public async Task RedirectToWebWhenFeatureIsDisabled() await handler.HandleBuyClickAsync(ITEM_URN, MARKETPLACE_URL, CreateVisuals(), _ => { }, CancellationToken.None); // Assert - webBrowser.Received(1).OpenUrlMainThreadOnly(MARKETPLACE_URL); + Assert.AreEqual(webBrowser.UrlOpened, MARKETPLACE_URL); await shopAPIClient.DidNotReceive().GetShopListingForItemAsync(Arg.Any(), Arg.Any(), Arg.Any()); } @@ -63,7 +65,7 @@ public async Task OpenPurchaseModalWhenListingIsCreditBuyable() // Assert await mvcManager.Received(1).ShowAsync(Arg.Any>(), Arg.Any()); - webBrowser.DidNotReceive().OpenUrlMainThreadOnly(Arg.Any()); + Assert.IsNull(webBrowser.UrlOpened); } [Test] @@ -79,7 +81,7 @@ public async Task RedirectToWebWhenItemHasNoCreditListing() await handler.HandleBuyClickAsync(ITEM_URN, MARKETPLACE_URL, CreateVisuals(), _ => { }, CancellationToken.None); // Assert - webBrowser.Received(1).OpenUrlMainThreadOnly(MARKETPLACE_URL); + Assert.AreEqual(webBrowser.UrlOpened, MARKETPLACE_URL); await mvcManager.DidNotReceive().ShowAsync(Arg.Any>(), Arg.Any()); } @@ -96,7 +98,7 @@ public async Task RedirectToWebWhenListingResolutionFails() await handler.HandleBuyClickAsync(ITEM_URN, MARKETPLACE_URL, CreateVisuals(), _ => { }, CancellationToken.None); // Assert - webBrowser.Received(1).OpenUrlMainThreadOnly(MARKETPLACE_URL); + Assert.AreEqual(webBrowser.UrlOpened, MARKETPLACE_URL); } [Test] @@ -109,7 +111,7 @@ public async Task RedirectToWebWhenUrnIsNotParseable() await handler.HandleBuyClickAsync("urn:decentraland:off-chain:base-avatars:brown_pants", MARKETPLACE_URL, CreateVisuals(), _ => { }, CancellationToken.None); // Assert - webBrowser.Received(1).OpenUrlMainThreadOnly(MARKETPLACE_URL); + Assert.AreEqual(webBrowser.UrlOpened, MARKETPLACE_URL); await shopAPIClient.DidNotReceive().GetShopListingForItemAsync(Arg.Any(), Arg.Any(), Arg.Any()); } @@ -164,5 +166,21 @@ public void ParseContractAndItemFromCollectionUrns() Assert.IsFalse(CreditPurchaseBuyHandler.TryParseCollectionItem("urn:decentraland:off-chain:base-avatars:brown_pants", out _, out _)); Assert.IsFalse(CreditPurchaseBuyHandler.TryParseCollectionItem("no-colons-here", out _, out _)); } + + private class MockWebBrowser : UnityAppWebBrowser + { + public string UrlOpened { get; private set; } = null; + + public MockWebBrowser() : base(Substitute.For()) + { + + } + + public override void OpenUrlMainThreadOnly(string url) => + UrlOpened = url; + + public override void OpenUrlMainThreadOnly(DecentralandUrl url) => + UrlOpened = url.ToString(); + } } } diff --git a/Explorer/Assets/DCL/NetworkDefinitions/Browser/UnityAppWebBrowser.cs b/Explorer/Assets/DCL/NetworkDefinitions/Browser/UnityAppWebBrowser.cs index 1f8912d3cb2..73266de52bf 100644 --- a/Explorer/Assets/DCL/NetworkDefinitions/Browser/UnityAppWebBrowser.cs +++ b/Explorer/Assets/DCL/NetworkDefinitions/Browser/UnityAppWebBrowser.cs @@ -14,12 +14,12 @@ public UnityAppWebBrowser(IDecentralandUrlsSource decentralandUrlsSource) this.decentralandUrlsSource = decentralandUrlsSource; } - public void OpenUrlMainThreadOnly(string url) + public virtual void OpenUrlMainThreadOnly(string url) { Application.OpenURL(Uri.EscapeUriString(url)); } - public void OpenUrlMainThreadOnly(DecentralandUrl url) + public virtual void OpenUrlMainThreadOnly(DecentralandUrl url) { OpenUrlMainThreadOnly(decentralandUrlsSource.Url(url)); } From 96e54e56b1218592cade45a9cf24f46c49db759e Mon Sep 17 00:00:00 2001 From: Nicolas Lorusso Date: Fri, 17 Jul 2026 12:01:40 -0300 Subject: [PATCH 54/54] reduce warnings --- .../AuthenticationScreenController.cs | 2 +- .../Dapp/DappDeepLinkAuthenticator.cs | 4 ++-- .../Implementations/Dapp/DappWeb3EthereumApi.cs | 2 +- .../Implementations/PrivateKeyAuthenticator.cs | 2 +- .../RandomGeneratedWeb3Authenticator.cs | 2 +- .../Implementations/TokenFileAuthenticator.cs | 2 +- Explorer/Assets/DCL/Web3/Identities/IWeb3Identity.cs | 12 ++++++------ ...landIdentityWithNethereumAccountJsonSerializer.cs | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs index f066b1ffbe0..35918d0b417 100644 --- a/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs +++ b/Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs @@ -212,7 +212,7 @@ private async UniTaskVoid TryAutoLoginAndProceedAsync(IWeb3Identity storedIdenti bool autoLoginSuccess = await web3Authenticator.TryAutoLoginAsync(ct); if (autoLoginSuccess) - fsm.Enter(new (storedIdentity, storedIdentity.Source != IWeb3Identity.Web3IdentitySource.TokenFile, ct)); + fsm.Enter(new (storedIdentity, storedIdentity.Source != IWeb3Identity.Web3IdentitySource.TOKEN_FILE, ct)); else { fsm.Enter(UIAnimationHashes.IN, true); diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs index bf06673b953..4c184abc351 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappDeepLinkAuthenticator.cs @@ -34,7 +34,7 @@ public class DappDeepLinkAuthenticator : IWeb3Authenticator private readonly ReactiveProperty deeplinkSigninIdentityId; private readonly ReactiveProperty loginAwaitingSigninRequestId; private readonly URLBuilder urlBuilder = new (); - private readonly DCLSemaphoreSlim loginMutex = new (1, 1); + private readonly DCLSemaphoreSlim loginMutex = new (); public DappDeepLinkAuthenticator( UnityAppWebBrowser webBrowser, @@ -166,7 +166,7 @@ private async UniTask FetchIdentityByIdAsync(string identi DateTime expiration = DateTime.Parse(json.identity.expiration, null, DateTimeStyles.RoundtripKind); - return new DecentralandIdentity(new Web3Address(signerAddress), ephemeralAccount, expiration, authChain, IWeb3Identity.Web3IdentitySource.Deeplink); + return new DecentralandIdentity(new Web3Address(signerAddress), ephemeralAccount, expiration, authChain, IWeb3Identity.Web3IdentitySource.DEEPLINK); } // Field names mirror the auth server's JSON payloads verbatim, so they intentionally break the naming rules. diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs index 500f3f4fe3f..3f147bcda02 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/Dapp/DappWeb3EthereumApi.cs @@ -191,7 +191,7 @@ private async UniTask LoginAsync(LoginPayload payload, Cancellati // To keep cohesiveness between the platform, convert the user address to lower case return new DecentralandIdentity(new Web3Address(response.sender), - ephemeralAccount, sessionExpiration, authChain, IWeb3Identity.Web3IdentitySource.Dapp); + ephemeralAccount, sessionExpiration, authChain, IWeb3Identity.Web3IdentitySource.DAPP); } catch (Exception) { diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/PrivateKeyAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/PrivateKeyAuthenticator.cs index 40a0333c38a..9594ef1e261 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/PrivateKeyAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/PrivateKeyAuthenticator.cs @@ -34,7 +34,7 @@ public void Dispose() { } public bool IsExpired => false; public AuthChain AuthChain { get; } - public IWeb3Identity.Web3IdentitySource Source { get; set; } = IWeb3Identity.Web3IdentitySource.None; + public IWeb3Identity.Web3IdentitySource Source { get; set; } = IWeb3Identity.Web3IdentitySource.NONE; public AuthChain Sign(string entityId) { diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/RandomGeneratedWeb3Authenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/RandomGeneratedWeb3Authenticator.cs index 4beec1ca5ee..2fb637fba6c 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/RandomGeneratedWeb3Authenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/RandomGeneratedWeb3Authenticator.cs @@ -45,7 +45,7 @@ public UniTask LoginAsync(LoginPayload payload, CancellationToken ephemeralAccount, expiration, authChain, - IWeb3Identity.Web3IdentitySource.None + IWeb3Identity.Web3IdentitySource.NONE ).AsUniTaskResult(); } diff --git a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TokenFileAuthenticator.cs b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TokenFileAuthenticator.cs index 7f3e9f933b8..b14af50a27c 100644 --- a/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TokenFileAuthenticator.cs +++ b/Explorer/Assets/DCL/Web3/Authenticators/Implementations/TokenFileAuthenticator.cs @@ -93,7 +93,7 @@ private async UniTask LoginAsync(CancellationToken ct) DateTime expiration = DateTime.Parse(json.identity.expiration, null, DateTimeStyles.RoundtripKind); return new DecentralandIdentity(new Web3Address(address), ephemeralAccount, expiration, authChain, - IWeb3Identity.Web3IdentitySource.TokenFile); + IWeb3Identity.Web3IdentitySource.TOKEN_FILE); } public UniTask RequestTransferAsync(string giftUrn, string recipientAddress, CancellationToken ct) diff --git a/Explorer/Assets/DCL/Web3/Identities/IWeb3Identity.cs b/Explorer/Assets/DCL/Web3/Identities/IWeb3Identity.cs index a7ccc4558d7..21aa395d288 100644 --- a/Explorer/Assets/DCL/Web3/Identities/IWeb3Identity.cs +++ b/Explorer/Assets/DCL/Web3/Identities/IWeb3Identity.cs @@ -18,12 +18,12 @@ public interface IWeb3Identity : IDisposable enum Web3IdentitySource { - None, - Cached, - TokenFile, - Dapp, + NONE, + CACHED, + TOKEN_FILE, + DAPP, OTP, - Deeplink + DEEPLINK, } class Random : IWeb3Identity @@ -56,7 +56,7 @@ private Random(Web3Address address, DateTime expiration, IWeb3Account ephemeralA public IWeb3Account EphemeralAccount { get; } public bool IsExpired { get; } public AuthChain AuthChain { get; } - public Web3IdentitySource Source { get; set; } = Web3IdentitySource.None; + public Web3IdentitySource Source { get; set; } = Web3IdentitySource.NONE; public AuthChain Sign(string entityId) => throw new Exception("RandomIdentity cannot sign anything"); diff --git a/Explorer/Assets/DCL/Web3/Identities/PlayerPrefsIdentityProvider.DecentralandIdentityWithNethereumAccountJsonSerializer.cs b/Explorer/Assets/DCL/Web3/Identities/PlayerPrefsIdentityProvider.DecentralandIdentityWithNethereumAccountJsonSerializer.cs index 6a48a08ae53..6809bf73594 100644 --- a/Explorer/Assets/DCL/Web3/Identities/PlayerPrefsIdentityProvider.DecentralandIdentityWithNethereumAccountJsonSerializer.cs +++ b/Explorer/Assets/DCL/Web3/Identities/PlayerPrefsIdentityProvider.DecentralandIdentityWithNethereumAccountJsonSerializer.cs @@ -35,7 +35,7 @@ public DecentralandIdentityWithNethereumAccountJsonSerializer(IWeb3AccountFactor accountFactory.CreateAccount(new EthECKey(jsonRoot.key)), DateTime.Parse(jsonRoot.expiration, null, DateTimeStyles.RoundtripKind), authChain, - IWeb3Identity.Web3IdentitySource.Cached); + IWeb3Identity.Web3IdentitySource.CACHED); } public string Serialize(IWeb3Identity identity)