Skip to content

feat: Clean up incoming deeplink Logic for Branch SDK and Linking API#27645

Open
MarioAslau wants to merge 26 commits into
mainfrom
feat/deeplink-cleanup
Open

feat: Clean up incoming deeplink Logic for Branch SDK and Linking API#27645
MarioAslau wants to merge 26 commits into
mainfrom
feat/deeplink-cleanup

Conversation

@MarioAslau
Copy link
Copy Markdown
Contributor

@MarioAslau MarioAslau commented Mar 18, 2026

Description

Cleans up the incoming deeplink logic for the Branch SDK and React Native Linking API, and fixes the X (Twitter) iOS deeplink bug where tapping a deeplink from X lands on "this page doesn't exist" or does nothing instead of navigating to the correct screen.

Why: The deeplink entry pipeline had redundancies, dead code, a bug in rewriteBranchUri, and a race condition where both the Linking API and Branch SDK could fire for the same incoming link. Additionally, when deeplinks arrive from X (Twitter) via Branch's Deepview intermediate page, the Branch SDK fails to resolve the short link — returning +clicked_branch_link=false and no $deeplink_path. This left the app with an unroutable Branch short link URL (e.g. metamask-alternate.app.link/1WkF6GmE40b) that matched no supported action. For links with query params (e.g. link.metamask.io/buy?chainId=59144&amount=25), X wraps them in a generic Branch link whose Deepview returns $deeplink_path: "open" — losing both the correct path and all query params.

What changed:

  1. Fix rewriteBranchUri return value — Returns undefined instead of the original URI when it cannot rewrite (when +clicked_branch_link is false or $deeplink_path is absent). Previously returned a truthy, unresolvable Branch short link that was dispatched as a deeplink and caused 404s.

  2. Remove .filter(Boolean) dead code — Removed unnecessary .filter(Boolean) from the METAMASK_HOSTS array in util/deeplinks/index.ts. Every entry already has an || fallback to a string literal, so no entry can ever be falsy.

  3. Eliminate redundant getLatestReferringParams callhandleUniversalLink was calling branch.getLatestReferringParams() a second time (with a 500ms timeout/Promise.race) for analytics context. Now Branch params are cached on the DeeplinkManager instance when first fetched and read directly in handleUniversalLink. Removed the react-native-branch and Logger imports from handleUniversalLink.ts.

  4. Resolve Linking API / Branch SDK race condition — Added isBranchDomainUrl() helper to identify Branch domain URLs (metamask.app.link, metamask-alternate.app.link). Both Linking.getInitialURL() and Linking.addEventListener('url') now skip Branch domain URLs, since those are exclusively handled by the Branch SDK. This prevents duplicate processing where both entry points would fire for the same link.

  5. Fix branch.subscribe fallback for unresolved short links — Previously, when rewriteBranchUri returned undefined in the branch.subscribe handler, the code fell back to opts.uri (the raw Branch short link). This caused the app to try routing a short link path like /1WkF6GmE40b as an action, which failed. Now the fallback only uses opts.uri when it's NOT a Branch domain URL.

  6. Add resolveBranchShortLink fallback for Deepview flows — When the Branch SDK returns +non_branch_link pointing to a Branch domain (which happens with X/Twitter Deepview links), the SDK has failed to resolve the short link. A new resolveBranchShortLink() function fetches the short link URL directly (using a crawler User-Agent so Branch returns the metadata page), follows redirects, and resolves the original deep link using a tiered fallback chain. This handles both the branch.subscribe warm-start path and the getBranchDeeplink cold-start path. Includes a 3-second AbortController timeout, domain validation on +non_branch_link, and .catch() on the promise chain.

  7. Add extractBranchUrlFromDeepview for reliable Deepview resolution — Branch Deepview HTML embeds the full original URL in the al:ios:url meta tag as a base64-encoded JSON blob inside the link_click_id parameter. This new function decodes that blob to extract the +url field (e.g. https://link.metamask.io/buy), which is the authoritative original URL configured in the Branch dashboard. This is the primary resolution method and is more reliable than regex-matching $deeplink_path or parsing CTA button hrefs from the Deepview HTML. Query params from the original Branch URL (e.g. chainId, address, amount) are merged onto the resolved +url.

  8. Add stripBranchDeepviewParams helper — Strips Branch Deepview query params (__branch_flow_type, __branch_flow_id, __branch_mobile_deepview_type, _referrer) from URLs before resolution. Preserves MetaMask's own signed deeplink params (sig, sig_params) and user-facing params. These Deepview params are appended by Branch's intermediate page and can interfere with link resolution.

  9. Add extractDeepviewPath fallback — Parses the Deepview CTA button href or window.top.location assignment for the deeplink path. Handles null-prefixed paths (e.g. nulltrending), metamask:// scheme, and full https://link.metamask.io/ URLs. Used as a last-resort fallback when link_click_id decoding and $deeplink_path regex both fail.

  10. Use crawler User-Agent for Branch metadata fetch — Branch serves different HTML based on User-Agent. Browser/app UAs get the visual Deepview page (no al:ios:url), while crawler UAs get the metadata page with al:ios:url containing the link_click_id. Changed the fetch User-Agent to facebookexternalhit/1.1 (MetaMask-LinkResolver) to ensure we receive the metadata page needed for link_click_id decoding.

Changelog

CHANGELOG entry: Fixed deeplinks from X (Twitter) on iOS not landing on the correct screen by decoding Branch Deepview metadata to recover the original deep link URL with full path and query params; resolved duplicate processing race condition between Branch SDK and Linking API; added 3-second timeout on Branch link resolution fetch

Related issues

Fixes: #27140
Refs: #27139
Jira Ticket: https://consensyssoftware.atlassian.net/browse/MCWP-386?atlOrigin=eyJpIjoiNjUyYjFmMmVkYWM3NDk1MDliNTNiYzFhYTE3YTcwZWUiLCJwIjoiaiJ9

Manual testing steps

Feature: Deeplinks from external apps

  Scenario: User taps a trending deeplink from X (Twitter) on iOS cold start
    Given the MetaMask app is not running
    When user taps the MetaMask trending deeplink shared on X (e.g. from @MetaMask tweet)
    And the Branch Deepview intermediate page is shown in X's in-app browser
    And user taps the "Open" / "Get the app" button on the Deepview
    Then the app opens and navigates to the Explore/Trending screen

  Scenario: User taps a trending deeplink from X (Twitter) on iOS warm start
    Given the MetaMask app is running in the background
    When user taps the MetaMask trending deeplink shared on X
    And the Branch Deepview intermediate page is shown
    And user taps the "Open" / "Get the app" button
    Then the app navigates to the Explore/Trending screen

  Scenario: User taps a buy deeplink with query params from X (Twitter) on iOS
    Given the MetaMask app is installed
    When user taps a link like https://link.metamask.io/buy?chainId=59144&address=0x...&amount=25 shared on X
    And the Branch Deepview intermediate page is shown
    And user taps the "Open" / "Get the app" button
    Then the app opens and navigates to the Buy screen with chainId, address, and amount pre-filled

  Scenario: User taps a deeplink from Slack on iOS cold start
    Given the MetaMask app is not running
    When user taps a MetaMask deeplink (e.g. https://link.metamask.io/buy) from Slack
    Then the app opens and navigates to the correct screen (Buy)

  Scenario: User taps a deeplink from Slack on iOS warm start
    Given the MetaMask app is running in the background
    When user taps a MetaMask deeplink from Slack
    Then the app navigates to the correct screen

  Scenario: User taps a deeplink from Slack on Android cold start
    Given the MetaMask app is not running
    When user taps a MetaMask deeplink from Slack
    Then the app opens and navigates to the correct screen

  Scenario: User taps a deeplink from Slack on Android warm start
    Given the MetaMask app is running in the background
    When user taps a MetaMask deeplink from Slack
    Then the app navigates to the correct screen

  Scenario: User taps a deeplink from X (Twitter) on Android cold start
    Given the MetaMask app is not running
    When user taps a MetaMask deeplink shared on X
    Then the app opens and navigates to the correct in-app destination

  Scenario: User taps a deeplink from X (Twitter) on Android warm start
    Given the MetaMask app is running in the background
    When user taps a MetaMask deeplink shared on X
    Then the app navigates to the correct in-app destination

  Scenario: Custom scheme deeplinks still work
    Given the MetaMask app is installed
    When user opens a metamask:// or ethereum: deeplink from any source
    Then the app handles it correctly via Linking API

  Scenario: Push notification deeplinks still work
    Given the MetaMask app receives a push notification with a deeplink
    When user taps the notification
    Then the app navigates to the correct screen

Screenshots/Recordings

Before

N/A (logic-only changes, no UI)

After

ScreenRecording_03-19-2026.20-31-28_1.1.MP4

N/A (logic-only changes, no UI)

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Changes core deeplink entry logic (Branch + React Native Linking) and adds network-based short-link resolution, which could affect navigation/attribution and introduce edge-case regressions despite extensive tests.

Overview
Improves incoming deeplink processing by preventing unroutable Branch short links from being dispatched: rewriteBranchUri now returns undefined when it can’t rewrite, and DeeplinkManager.start no longer falls back to handling raw Branch-domain URLs.

Adds Branch-specific utilities to avoid duplicate processing and recover Deepview/X (Twitter) links: isBranchDomainUrl filters Branch domains out of the React Native Linking pipeline, and resolveBranchShortLink fetches/decodes Deepview metadata (including link_click_id+url) with param merging, Deepview-param stripping, and timeouts.

Branch params are now cached on DeeplinkManager and consumed by handleUniversalLink for analytics (removing extra Branch SDK calls). Tests were expanded significantly to cover cold/warm start flows, error handling, and Deepview resolution; bitrise.yml build numbers were bumped.

Written by Cursor Bugbot for commit e7ecd63. This will update automatically on new commits. Configure here.

@MarioAslau MarioAslau requested a review from Cal-L March 18, 2026 18:57
@MarioAslau MarioAslau self-assigned this Mar 18, 2026
@MarioAslau MarioAslau added the team-mobile-platform Mobile Platform team label Mar 18, 2026
@github-actions
Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@github-actions github-actions Bot added size-M risk-medium Moderate testing recommended · Possible bug introduction risk labels Mar 18, 2026
@github-actions github-actions Bot added risk-high Extensive testing required · High bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Mar 18, 2026
@MarioAslau MarioAslau marked this pull request as ready for review March 18, 2026 21:55
@MarioAslau MarioAslau requested a review from a team as a code owner March 18, 2026 21:55
Comment thread app/core/DeeplinkManager/DeeplinkManager.ts
Comment thread app/core/DeeplinkManager/DeeplinkManager.ts Outdated
Comment thread app/core/DeeplinkManager/DeeplinkManager.ts
Comment thread app/core/DeeplinkManager/DeeplinkManager.test.ts
Comment thread app/core/DeeplinkManager/DeeplinkManager.ts Outdated
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-high Extensive testing required · High bug introduction risk labels Mar 19, 2026
@github-actions github-actions Bot added size-L risk-medium Moderate testing recommended · Possible bug introduction risk and removed size-M risk-medium Moderate testing recommended · Possible bug introduction risk labels Mar 19, 2026
Comment thread app/core/DeeplinkManager/DeeplinkManager.ts Outdated
Comment thread app/core/DeeplinkManager/DeeplinkManager.ts Outdated
@github-actions github-actions Bot added the risk-medium Moderate testing recommended · Possible bug introduction risk label Mar 20, 2026
Comment thread app/core/DeeplinkManager/DeeplinkManager.ts
Comment thread bitrise.yml
Comment thread app/core/DeeplinkManager/DeeplinkManager.ts
Comment thread app/core/DeeplinkManager/DeeplinkManager.ts Outdated
@github-actions github-actions Bot added risk-high Extensive testing required · High bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Mar 20, 2026
@github-actions github-actions Bot added risk-high Extensive testing required · High bug introduction risk and removed risk-high Extensive testing required · High bug introduction risk labels Mar 20, 2026
Copy link
Copy Markdown
Contributor

@NicolasMassart NicolasMassart left a comment

Choose a reason for hiding this comment

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

Strong unit coverage. I’d clear the AbortController timeout in a finally though.
Also I’d still skim Sonar’s new issues on the PR even though the quality gate passed.

Comment thread app/core/DeeplinkManager/DeeplinkManager.ts Outdated
@github-actions github-actions Bot added risk-high Extensive testing required · High bug introduction risk and removed risk-high Extensive testing required · High bug introduction risk labels Mar 20, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Comment thread app/core/DeeplinkManager/DeeplinkManager.ts
@github-actions github-actions Bot added risk-high Extensive testing required · High bug introduction risk and removed risk-high Extensive testing required · High bug introduction risk labels Mar 20, 2026
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-high Extensive testing required · High bug introduction risk labels Mar 20, 2026
@github-actions github-actions Bot added risk-high Extensive testing required · High bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Mar 20, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeAccounts, SmokeConfirmations, SmokeIdentity, SmokeNetworkAbstractions, SmokeNetworkExpansion, SmokeTrade, SmokeWalletPlatform, SmokeCard, SmokePerps, SmokeRamps, SmokeMultiChainAPI, SmokePredictions, FlaskBuildTests
  • Selected Performance tags: @PerformanceAccountList, @PerformanceOnboarding, @PerformanceLogin, @PerformanceSwaps, @PerformanceLaunch, @PerformanceAssetLoading, @PerformancePredict, @PerformancePreps
  • Risk Level: high
  • AI Confidence: %
click to see 🤖 AI reasoning details

E2E Test Selection:
Fallback: AI analysis did not complete successfully. Running all tests.

Performance Test Selection:
Fallback: AI analysis did not complete successfully. Running all performance tests.

View GitHub Actions results

@github-actions
Copy link
Copy Markdown
Contributor

E2E Fixture Validation — Schema is up to date
17 value mismatches detected (expected — fixture represents an existing user).
View details

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@tommasini tommasini left a comment

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk-high Extensive testing required · High bug introduction risk size-XL team-mobile-platform Mobile Platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Deeplinks not working from X (twitter) in app browser

4 participants