Skip to content

fix: improve deep-link launch detection#431

Merged
LautaroPetaccio merged 2 commits into
mainfrom
fix/deep-link-detection
Jul 13, 2026
Merged

fix: improve deep-link launch detection#431
LautaroPetaccio merged 2 commits into
mainfrom
fix/deep-link-detection

Conversation

@LautaroPetaccio

Copy link
Copy Markdown
Collaborator

Summary

  • extend the desktop deep-link detection window from 500 ms to 5 seconds so users can respond to the native-app prompt
  • detect successful handoff through blur, visibilitychange, and pagehide lifecycle signals
  • clean up listeners, timers, and the hidden iframe after detection settles
  • add direct unit coverage for each desktop signal, timeout cleanup, and mobile navigation

Problem

The previous detector treated the absence of window blur within 500 ms as a launch failure. Native-protocol prompts can remain open longer than that, causing the page to show an application-opening error even though the app opens after the user accepts the prompt.

Validation

  • TypeScript: ./node_modules/.bin/tsc --noEmit
  • ESLint: changed files pass
  • Jest: 22 focused tests pass
  • git diff --check

@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
auth Ready Ready Preview, Comment Jul 13, 2026 7:27pm

Request Review

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

PR: #431 — fix: improve deep-link launch detection
Files: 2 changed (+163 −16)
Branch: fix/deep-link-detectionmain

Verdict: ✅ Approve

Clean, well-structured fix addressing a real UX issue. No P0 or P1 findings.

What this PR does

Extends deep-link detection from a single blur event within 500ms to three browser lifecycle signals (blur, visibilitychange, pagehide) within 5 seconds. This prevents a false "app not found" error when native-protocol confirmation dialogs take longer than 500ms to dismiss. The settle/cleanup pattern correctly prevents double-resolution of the Promise and ensures thorough resource cleanup (all listeners removed, timeout cleared, iframe removed).

Analysis

  • Correctness: The settled boolean guard is the standard idiom for one-shot promise resolution. The execution ordering (listeners → setTimeout → appendChild) is correct — listeners are attached before the iframe triggers the deep link, preventing a race where a fast app launch could fire blur before the handler is registered.
  • API contract: Unchanged (url: string) => Promise<boolean>. Both consumers (ContinueInApp.tsx, MobileAuthSuccess.tsx) handle true/false outcomes identically — no caller impact.
  • Browser compatibility: visibilitychange (IE10+) and pagehide (all evergreen browsers, Safari included) have broad support. Together they cover cases blur misses, particularly mobile WebViews.
  • Security: No new vulnerabilities. No secrets, XSS vectors, or auth changes. (Pre-existing: the url parameter has no protocol-scheme guard — not introduced here, but worth a follow-up url.startsWith('decentraland://') check.)
  • Tests: 128-line test file covers all five main paths (blur, visibilitychange, pagehide, timeout, mobile). Proper teardown with useRealTimers, resetAllMocks, and DOM cleanup.

P2 — Minor observations (non-blocking)

  1. [Style] timeoutId declaration orderingcleanup() references timeoutId before its const declaration (line 251). This is safe today because cleanup() is only called asynchronously after assignment, but a future refactor could introduce a TDZ ReferenceError. Declaring let timeoutId: ReturnType<typeof setTimeout> above cleanup would be more robust.

  2. [Test coverage] No double-signal guard test — No test fires two signals in sequence (e.g., blur then visibilitychange) to verify the promise resolves only once. The settled flag handles this correctly, but an explicit test would harden the invariant.

  3. [Test coverage] visibilitychange with visible state — No test confirms the handler is a no-op when visibilityState is 'visible'. Worth adding for completeness.

  4. [UX tradeoff] 5s timeout — Well justified in the code comment and PR description. If the desktop app is not installed, the user now waits 5s before seeing the failure state. This is a reasonable tradeoff given that 500ms caused false negatives.

  5. [Heuristic limitation] False positive from manual tab switch — If the user switches tabs within 5s for reasons unrelated to the deep link, visibilitychange would resolve true. This is inherent to heuristic detection (the old blur-only approach had the same class of issue) and is acceptable given the context.

CI Status

All checks passing (audit ✅, test ✅, CodeQL ✅, lint ✅, Vercel ✅). E2e tests were still in progress at review time.


Reviewed by Jarvis 🤖 · Requested by Lautaro Petaccio (<@U2MAY5BHL>) via Slack

@LautaroPetaccio LautaroPetaccio merged commit 21e2097 into main Jul 13, 2026
14 of 15 checks passed
@LautaroPetaccio LautaroPetaccio deleted the fix/deep-link-detection branch July 13, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants