Skip to content

Escape store links from in-app browsers - #4274

Draft
pepeladeira wants to merge 7 commits into
mainfrom
escape-store-links-in-app-browsers
Draft

Escape store links from in-app browsers#4274
pepeladeira wants to merge 7 commits into
mainfrom
escape-store-links-in-app-browsers

Conversation

@pepeladeira

@pepeladeira pepeladeira commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Added an in-app browser escape experience for Instagram, Facebook, and TikTok.
    • Users can open links in an external browser or copy destination URLs.
    • Added localized messaging in eight languages, app store guidance, branded styling, badges, and deep-link support.
  • Bug Fixes
    • Improved redirect handling across iOS, Android, and location-based destinations.
    • Added clear success messaging and a selectable fallback URL when copying fails.
    • Invalid or unsupported links now continue safely without unnecessary external-browser redirects.

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Aug 4, 2026 8:05pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an in-app browser escape flow for Instagram, Facebook, and TikTok browser contexts. The middleware creates encoded escape URLs, and the escape page renders localized messaging, branding, destination details, and browser or copy actions.

Changes

In-app browser escape

Layer / File(s) Summary
Detect and route in-app browsers
apps/web/lib/middleware/utils/detect-in-app-browser.ts, apps/web/lib/middleware/link.ts
The middleware detects supported in-app browsers for store destinations. It resolves the destination, records the click, and rewrites the request to an encoded escape URL.
Render localized escape page
apps/web/app/in-app-browser/[url]/page.tsx, apps/web/app/in-app-browser/[url]/translations.ts
The escape page decodes and validates request data, resolves optional link configuration, selects localized content, and renders branding and store messaging.
Provide browser and copy actions
apps/web/app/in-app-browser/[url]/action-button.tsx
The client component opens external-browser schemes, handles intent:// fallbacks, copies the destination URL, and displays a selectable URL fallback when copying fails.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant LinkMiddleware
  participant EscapePage
  participant Prisma
  participant ActionButton

  Request->>LinkMiddleware: request store destination
  LinkMiddleware->>LinkMiddleware: detect supported in-app browser
  LinkMiddleware-->>Request: rewrite to encoded escape URL
  Request->>EscapePage: load escape URL
  EscapePage->>Prisma: resolve optional link configuration
  Prisma-->>EscapePage: return configuration or destination fallback
  EscapePage->>ActionButton: provide destination and copy URLs
  ActionButton-->>Request: open external browser or copy URL
Loading

Possibly related PRs

  • dubinc/dub#3836: Modifies shared middleware redirect handling and destination selection.
  • dubinc/dub#3946: Modifies related deep-view branding, configuration, and button styling.
  • dubinc/dub#3997: Adds related intent:// fallback behavior with visibility detection.

Suggested reviewers: devkiran, steven-tey

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: escaping store links from in-app browsers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch escape-store-links-in-app-browsers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

🧹 Nitpick comments (2)
apps/web/app/in-app-browser/[url]/page.tsx (2)

36-40: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Falling back to "instagram" for an unknown source is misleading.

Line 81 coerces any unrecognized source value to "instagram". The page then emits an instagram://extbrowser/ scheme and auto-navigates to it at Line 151. On a device without Instagram installed, that navigation fails and the user sees nothing happen.

Treat an unknown source as "no scheme available" instead, so the page renders the copy-link fallback.

♻️ Proposed change to the source fallback
-  const source: InAppBrowserSource = VALID_SOURCES.has(
-    rawSource as InAppBrowserSource,
-  )
-    ? (rawSource as InAppBrowserSource)
-    : "instagram";
+  const source: InAppBrowserSource | null = VALID_SOURCES.has(
+    rawSource as InAppBrowserSource,
+  )
+    ? (rawSource as InAppBrowserSource)
+    : null;

getExtBrowserScheme then needs to accept null and return null for it.

Also applies to: 77-81

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/in-app-browser/`[url]/page.tsx around lines 36 - 40, The source
fallback logic (around the VALID_SOURCES reference) currently coerces
unrecognized source values to "instagram", which triggers a misleading
instagram:// scheme navigation that fails if the app isn't installed. Instead of
defaulting unknown sources to a valid scheme, treat them as null to represent
"no scheme available". Update the getExtBrowserScheme function to accept null as
input and return null for it, so the page falls back to rendering the copy-link
UI for unrecognized sources rather than attempting a failed navigation.

105-130: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove the duplicate Prisma lookup from the escape page.

The in-app browser rewrite only passes domain and key through search params, so apps/web/app/in-app-browser/[url]/page.tsx resolves shortLink and shortDomain.deepviewData again. Move those fields into the rewrite state, or avoid re-fetching when they are already available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/in-app-browser/`[url]/page.tsx around lines 105 - 130, The
Prisma findUnique query around the domain_key lookup is redundantly fetching
shortLink and shortDomain.deepviewData even though the rewrite already passes
domain and key through search params. Either include shortLink and deepviewData
in the rewrite state to make them available without re-fetching, or
conditionally skip the findUnique query if those fields are already present.
Preserve the decodeLinkIfCaseSensitive logic for decoding the resolved link
data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/in-app-browser/`[url]/action-button.tsx:
- Around line 38-46: Update the button styling in the action-button component so
its text color remains readable when buttonStyle.backgroundColor is customized.
Derive the label color from the configured background or apply a
buttonStyle.color override, while preserving the existing white-text fallback
when no custom styling is provided.
- Around line 48-53: The copy button in the action-button component fails
silently when the Clipboard API is unavailable, leaving the user with no
feedback. Update the onClick handler for the Button to pass throwOnError to
copyToClipboard (or configure useCopyToClipboard with throwOnError), then add
error state tracking alongside the existing copied state to capture and display
failures. When an error occurs, show an error message or visual indicator to the
user so they know the clipboard action failed, or alternatively render the
copyUrl as selectable text below the button so users can manually select and
copy the URL when the Clipboard API is unavailable.

In `@apps/web/app/in-app-browser/`[url]/page.tsx:
- Around line 54-66: The TikTok path in getExtBrowserScheme currently returns
null, causing InAppBrowserActionButton to navigate within the embedded webview
while still displaying the “Open in App Store” action. Update the related
action-button behavior so the primary action is hidden when extBrowserScheme is
null, while preserving the copy-link fallback.
- Around line 93-95: Remove the decodeURIComponent call from the key assignment
in the page component, using searchParams.key directly when present and
preserving undefined when absent. Keep the existing key lookup flow unchanged so
literal percent characters are handled safely and the original decoded value
reaches findUnique.

In `@apps/web/app/in-app-browser/`[url]/translations.ts:
- Around line 25-26: Update the localized description and openInStore templates
in the French, Portuguese, and German translation entries to avoid fixed
articles that conflict with either storeName value. Remove or replace the
surrounding article phrasing while preserving the {storeName} interpolation and
the intended meaning for both Google Play and App Store.

In `@apps/web/lib/middleware/link.ts`:
- Around line 420-433: Consolidate destination selection and final URL
computation in the surrounding link middleware: reuse the existing precedence
logic from the branches at Lines 472, 536, and 599 instead of maintaining the
duplicate destinationForEscape chain. Compute getFinalUrl once for the resolved
destination, then pass the shared destination and final URL through those
branches so each request avoids repeated resolution and URL generation.
- Around line 435-469: Update the inAppBrowserSource escape branch before its
early return to call cacheDeepLinkClickData for eligible App Store or Play Store
destinations when skip_deeplink_preview is absent. Pass the main destination URL
rather than escapeFinalUrl so deferred deep-link tracking can recover the
original application URL.

---

Nitpick comments:
In `@apps/web/app/in-app-browser/`[url]/page.tsx:
- Around line 36-40: The source fallback logic (around the VALID_SOURCES
reference) currently coerces unrecognized source values to "instagram", which
triggers a misleading instagram:// scheme navigation that fails if the app isn't
installed. Instead of defaulting unknown sources to a valid scheme, treat them
as null to represent "no scheme available". Update the getExtBrowserScheme
function to accept null as input and return null for it, so the page falls back
to rendering the copy-link UI for unrecognized sources rather than attempting a
failed navigation.
- Around line 105-130: The Prisma findUnique query around the domain_key lookup
is redundantly fetching shortLink and shortDomain.deepviewData even though the
rewrite already passes domain and key through search params. Either include
shortLink and deepviewData in the rewrite state to make them available without
re-fetching, or conditionally skip the findUnique query if those fields are
already present. Preserve the decodeLinkIfCaseSensitive logic for decoding the
resolved link data.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9bcbdeeb-5283-41aa-82c4-475fee916995

📥 Commits

Reviewing files that changed from the base of the PR and between 73c5809 and 5d70266.

📒 Files selected for processing (5)
  • apps/web/app/in-app-browser/[url]/action-button.tsx
  • apps/web/app/in-app-browser/[url]/page.tsx
  • apps/web/app/in-app-browser/[url]/translations.ts
  • apps/web/lib/middleware/link.ts
  • apps/web/lib/middleware/utils/detect-in-app-browser.ts

Comment thread apps/web/app/in-app-browser/[url]/action-button.tsx Outdated
Comment thread apps/web/app/in-app-browser/[url]/action-button.tsx
Comment thread apps/web/app/in-app-browser/[url]/page.tsx
Comment thread apps/web/app/in-app-browser/[url]/page.tsx Outdated
Comment thread apps/web/app/in-app-browser/[url]/translations.ts Outdated
Comment thread apps/web/lib/middleware/link.ts Outdated
Comment thread apps/web/lib/middleware/link.ts

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
apps/web/app/in-app-browser/[url]/page.tsx (1)

26-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused static rendering config, or document why it stays.

InAppBrowserEscapePage reads request data with headers() before getLanguage() and userAgent(). That makes the route dynamic, so revalidate = false and generateStaticParams() do not affect rendering. Keep the config if a specific reason applies; otherwise remove it to avoid a future headers() removal accidentally enabling stale static cache.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/in-app-browser/`[url]/page.tsx around lines 26 - 30, Remove the
unused revalidate export and generateStaticParams function near
InAppBrowserEscapePage, since headers() makes this route dynamic; only retain
them if you add documentation explaining a specific intentional purpose.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/web/app/in-app-browser/`[url]/page.tsx:
- Around line 26-30: Remove the unused revalidate export and
generateStaticParams function near InAppBrowserEscapePage, since headers() makes
this route dynamic; only retain them if you add documentation explaining a
specific intentional purpose.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4cf66a69-b8c2-4ab2-ba63-e40ed7c77596

📥 Commits

Reviewing files that changed from the base of the PR and between cde30e5 and 2aaf29b.

📒 Files selected for processing (3)
  • apps/web/app/in-app-browser/[url]/action-button.tsx
  • apps/web/app/in-app-browser/[url]/page.tsx
  • apps/web/lib/middleware/link.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/in-app-browser/[url]/action-button.tsx

@pepeladeira

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review. any breaking changes?

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git diff --stat HEAD^ HEAD
git diff --find-renames -- \
  'apps/web/lib/middleware/link.ts' \
  'apps/web/lib/middleware/utils/detect-in-app-browser.ts' \
  'apps/web/app/in-app-browser/[url]/action-button.tsx' \
  'apps/web/app/in-app-browser/[url]/page.tsx' \
  'apps/web/app/in-app-browser/[url]/translations.ts'

printf '\n--- Relevant symbols and references ---\n'
ast-grep outline apps/web/lib/middleware/link.ts --items all
ast-grep outline apps/web/lib/middleware/utils/detect-in-app-browser.ts --items all
ast-grep outline 'apps/web/app/in-app-browser/[url]/page.tsx' --items all
rg -n -C 3 '\b(shouldEscapeInAppBrowser|getInAppBrowserEscapeUrl|InAppBrowserEscapePage|InAppBrowserActionButton)\b' apps/web

printf '\n--- Related test files ---\n'
fd -t f . apps/web | rg -i '(in-app-browser|detect-in-app-browser|middleware.*link|link.*middleware).*(test|spec)|\.(test|spec)\.[^.]+$' || true

Length of output: 14052


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PR commits and changed paths ---'
git log --oneline HEAD^..HEAD
git diff --name-status HEAD^ HEAD

printf '%s\n' '--- Current LinkMiddleware redirect section ---'
sed -n '300,510p' apps/web/lib/middleware/link.ts
printf '%s\n' '--- Parent LinkMiddleware redirect section ---'
git show HEAD^:apps/web/lib/middleware/link.ts | sed -n '300,510p'

printf '%s\n' '--- In-app browser helpers ---'
cat -n apps/web/lib/middleware/utils/detect-in-app-browser.ts
printf '%s\n' '--- URL/store helpers ---'
cat -n apps/web/lib/middleware/utils/is-ios-app-store-url.ts
cat -n apps/web/lib/middleware/utils/is-google-play-store-url.ts
cat -n apps/web/lib/middleware/utils/get-final-url.ts

printf '%s\n' '--- Escape page and action component ---'
sed -n '1,340p' 'apps/web/app/in-app-browser/[url]/page.tsx'
sed -n '1,220p' 'apps/web/app/in-app-browser/[url]/action-button.tsx'

printf '%s\n' '--- Effective diff with context ---'
git diff --find-renames -U80 HEAD^ HEAD -- \
  'apps/web/lib/middleware/link.ts' \
  'apps/web/app/in-app-browser/[url]/page.tsx' \
  'apps/web/app/in-app-browser/[url]/action-button.tsx'

Length of output: 57039


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD main)"
printf 'Merge base: %s\n' "$base"
printf '%s\n' '--- Complete PR changed paths ---'
git diff --name-status "$base" HEAD
printf '%s\n' '--- Complete PR diff summary ---'
git diff --stat "$base" HEAD

printf '%s\n' '--- Complete PR diff: in-app-browser and middleware ---'
git diff -U50 "$base" HEAD -- \
  'apps/web/lib/middleware/link.ts' \
  'apps/web/lib/middleware/utils/detect-in-app-browser.ts' \
  'apps/web/app/in-app-browser/[url]/page.tsx' \
  'apps/web/app/in-app-browser/[url]/action-button.tsx' \
  'apps/web/app/in-app-browser/[url]/translations.ts'

printf '%s\n' '--- Middleware and route configuration references ---'
rg -n -C 4 'LinkMiddleware|matcher|in-app-browser|/deeplink/' \
  apps/web/middleware.ts apps/web/next.config.* apps/web 2>/dev/null || true

printf '%s\n' '--- Target branch versions, when files exist ---'
for f in \
  'apps/web/lib/middleware/utils/detect-in-app-browser.ts' \
  'apps/web/app/in-app-browser/[url]/page.tsx' \
  'apps/web/app/in-app-browser/[url]/action-button.tsx' \
  'apps/web/app/in-app-browser/[url]/translations.ts'
do
  if git cat-file -e "$base:$f" 2>/dev/null; then
    printf '\n--- %s at merge base ---\n' "$f"
    git show "$base:$f" | sed -n '1,360p'
  fi
done

printf '%s\n' '--- URL construction edge cases ---'
python3 - <<'PY'
from urllib.parse import quote
for u in [
    "https://play.google.com/store/apps/details?id=com.example#reviews",
    "https://apps.apple.com/us/app/example/id123?pt=1#privacy",
    "https://play.google.com:8443/store/apps/details?id=com.example",
]:
    print(u)
    print("encoded path:", quote(u, safe=""))
PY

Length of output: 45658


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Middleware dispatch and application-route exclusions ---'
sed -n '1,115p' apps/web/middleware.ts
sed -n '1,95p' apps/web/lib/middleware/app.ts

printf '%s\n' '--- Domain and request parsing behavior ---'
rg -n -C 3 '\bAPP_DOMAIN\b|function isDubDomain|const isDubDomain|export.*isDubDomain' \
  packages apps/web 2>/dev/null || true
cat -n apps/web/lib/middleware/utils/parse.ts

printf '%s\n' '--- Existing test conventions for middleware utilities ---'
fd -t f -e ts -e tsx apps/web | rg '(test|spec)\.(ts|tsx)$' | head -80
rg -n -C 2 'NextRequest|NextFetchEvent|LinkMiddleware|userAgent\(' \
  apps/web/tests apps/web/lib --glob '*.{test,spec}.ts' 2>/dev/null || true

printf '%s\n' '--- Package scripts and test tooling declarations ---'
for f in package.json apps/web/package.json; do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    sed -n '1,220p' "$f"
  fi
done

Length of output: 64519


@pepeladeira I completed the full review.

I found no breaking public API or redirect-contract change.

The PR intentionally changes behavior for Instagram, Facebook, and TikTok in-app browsers when the selected destination is an App Store or Google Play URL. These requests now render the escape page instead of continuing through the normal store-link flow.

For all other requests, the existing destination priority remains unchanged:

  1. iOS destination on iOS.
  2. Android destination on Android.
  3. Geo-targeted destination.
  4. Default destination.

The implementation preserves click recording, cookies, tracking parameters, and deferred deep-link caching before it enters the escape flow.

I did not identify a blocking code defect. The main release risk is device-specific behavior for the Instagram, Facebook, and Android intent:// external-browser schemes. Validate those paths on physical iOS and Android devices before release. Focused automated coverage for user-agent detection and escape URL construction would also reduce regression risk.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/in-app-browser/`[url]/translations.ts:
- Around line 86-104: Update the language validation in getLanguage to accept
only translation codes that are own properties of translations, using
Object.prototype.hasOwnProperty.call or an equivalent valid-code Set. Preserve
the existing fallback to "en" and return behavior for supported language codes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 512a4df5-220b-4b9f-be5a-91057bbbdaac

📥 Commits

Reviewing files that changed from the base of the PR and between cbc78e2 and 2aaf29b.

📒 Files selected for processing (5)
  • apps/web/app/in-app-browser/[url]/action-button.tsx
  • apps/web/app/in-app-browser/[url]/page.tsx
  • apps/web/app/in-app-browser/[url]/translations.ts
  • apps/web/lib/middleware/link.ts
  • apps/web/lib/middleware/utils/detect-in-app-browser.ts

Comment thread apps/web/app/in-app-browser/[url]/translations.ts
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.

1 participant