Skip to content

refactor: speed up deeplinks handling on cold start [GE-207]#29324

Merged
baptiste-marchand merged 19 commits into
mainfrom
refactor/speed-up-deeplinks
May 26, 2026
Merged

refactor: speed up deeplinks handling on cold start [GE-207]#29324
baptiste-marchand merged 19 commits into
mainfrom
refactor/speed-up-deeplinks

Conversation

@baptiste-marchand
Copy link
Copy Markdown
Contributor

@baptiste-marchand baptiste-marchand commented Apr 24, 2026

Description

This PR makes deeplink handling significantly faster, especially on cold start.

When an external deeplink (for example https://link.metamask.io/buy) opened the app from a killed state, users would first land on the home screen and the deeplink could take 3-5 seconds before navigating to the intended screen.

Root cause

The main issue was that SDKConnect / WalletConnect initialization blocked every post-login deeplink. handleDeeplinkSaga previously called initializeSDKServices before handling the pending deeplink, serializing WC2Manager.init and SDKConnect.init in front of every deeplink. This added several seconds on cold start, even for deeplinks such as buy / swap / perps / rewards that do not need those services.

Once that was fixed, it exposed a navigation readiness race. The root NavigationContainer can be ready before the post-login MainNavigator has mounted and registered screens such as Wallet, Ramp, Bridge, Perps, etc. The previous fixed setTimeout(..., 200) was not reliable enough on cold start, so navigate(...) could run while the target screen was not registered yet.

Changes

Non-blocking SDK initialization

initializeSDKServices now initializes WC2Manager and SDKConnect in parallel and is forked from handleDeeplinkSaga instead of blocking deeplink parsing.

This means non-SDK deeplinks no longer wait for WC2 / SDKConnect startup. SDK-specific paths still remain safe because handleMetaMaskDeeplink awaits the idempotent SDKConnect.init({ context: 'deeplink' }) when it actually needs SDKConnect.

MainNavigator readiness gating

Post-login deeplinks now go through parseDeeplinkAfterNavReady.

Instead of relying on a fixed 200 ms timeout, MainNavigator dispatches a new MAIN_NAVIGATOR_READY action when it mounts. The deeplink saga waits for that signal before parsing post-login deeplinks, with a 3 s safety timeout to avoid silently dropping the deeplink if MainNavigator never mounts.

This is intentionally different from ON_NAVIGATION_READY: ON_NAVIGATION_READY only means the root navigation container exists, while MAIN_NAVIGATOR_READY means the post-login app screens are registered.

New-user onboarding deeplinks are excluded from this wait because onboarding lives outside MainNavigator; they keep the previous fast onboarding behavior.

Fire-and-forget Branch.io params fetch

handleUniversalLink no longer awaits branch.getLatestReferringParams() before routing. It now passes a branchParamsPromise through the analytics context, preserving the existing 500 ms timeout/error handling while allowing navigation to proceed immediately.

Changelog

CHANGELOG entry: Improved deeplinks handling speed on cold-starts

Related issues

Fixes: https://consensyssoftware.atlassian.net/browse/GE-207

Manual testing steps

On Android (primary) and iOS, from a killed app state:

  • Tap https://link.metamask.io/buy from another app → lands on RampTokenSelection within ~500 ms of unlock.
  • Same for /swap, /perps, /home, /asset/..., /rewards.
  • wc://... WalletConnect deeplink still pairs correctly.
  • SDKv1 metamask://connect?... still connects.
  • MWP deeplinks unchanged.
  • Warm-start deeplink (app already in Wallet) still parses immediately (fast path).
  • Onboarding deeplink (/onboarding?type=google) for new users still works.

Screenshots/Recordings

Before

After

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

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
Touches core deeplink saga ordering, navigation timing, and SDK/WC init concurrency; wrong gating could drop or mis-route links, though timeouts and SDK-only waits mitigate that.

Overview
Cold-start deeplinks no longer block on WalletConnect / SDKConnect startup for ordinary links. The deeplink saga forks parallel WC2Manager + SDKConnect init and only joins that work when isSDKServiceDeeplink matches (WC, connect, mmsdk, etc.).

Navigation race fix: MainNavigator dispatches MAIN_NAVIGATOR_READY on mount. Post-login parsing goes through parseDeeplinkAfterNavReady, which waits for that action (or a 3s fallback) instead of a fixed 200ms setTimeout, so targets like Ramp/Wallet are registered before navigate. Onboarding /onboarding stays on the fast path outside this wait.

Universal links: Branch.io params are fetched via branchParamsPromise (500ms race) so routing/interstitials are not blocked; analytics resolves the promise later. handleMetaMaskDeeplink is async with .catch logging from handleUniversalLink. Shared helpers isMetaMaskSDKDeeplinkAction, isSDKServiceDeeplink, and isMetaMaskUniversalLink centralize host/action checks.

Tests cover saga timing, nav-ready gating, parallel SDK init, Branch analytics, and reducer behavior for the new action.

Reviewed by Cursor Bugbot for commit fd1652e. Bugbot is set up for automated code reviews on this repo. Configure here.

@baptiste-marchand baptiste-marchand requested review from a team as code owners April 24, 2026 12:46
@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.

Comment thread app/store/sagas/index.ts Outdated
@baptiste-marchand baptiste-marchand force-pushed the refactor/speed-up-deeplinks branch from d71b34a to 7041d23 Compare April 24, 2026 14:15
@baptiste-marchand baptiste-marchand changed the title Refactor: speed up deeplinks refactor: speed up deeplinks handling on cold start Apr 24, 2026
@baptiste-marchand baptiste-marchand changed the title refactor: speed up deeplinks handling on cold start refactor: speed up deeplinks handling on cold start [GE-207] Apr 24, 2026
Comment thread app/core/DeeplinkManager/handlers/legacy/handleMetaMaskDeeplink.ts
Comment thread app/core/DeeplinkManager/handlers/legacy/handleMetaMaskDeeplink.ts Outdated
samir-acle
samir-acle previously approved these changes Apr 27, 2026
Copy link
Copy Markdown
Contributor

@samir-acle samir-acle left a comment

Choose a reason for hiding this comment

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

Works great, nice job! 👏

vinnyhoward
vinnyhoward previously approved these changes Apr 27, 2026
@baptiste-marchand baptiste-marchand moved this to Has approvals, needs CODEOWNER in PR review queue Apr 28, 2026
@baptiste-marchand baptiste-marchand dismissed stale reviews from vinnyhoward and samir-acle via b88da46 May 19, 2026 12:40
@baptiste-marchand baptiste-marchand force-pushed the refactor/speed-up-deeplinks branch from ab4da31 to b88da46 Compare May 19, 2026 12:40
Comment thread app/store/sagas/index.ts Outdated
Comment thread app/store/sagas/index.ts
@metamaskbotv2 metamaskbotv2 Bot added the INVALID-PR-TEMPLATE PR's body doesn't match template label May 20, 2026
samir-acle
samir-acle previously approved these changes May 20, 2026
Copy link
Copy Markdown
Contributor

@samir-acle samir-acle left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Copy Markdown
Contributor

@Cal-L Cal-L left a comment

Choose a reason for hiding this comment

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

left some comments

Comment thread app/store/sagas/index.ts Outdated
* has rendered.
*/
export function* parseDeeplinkAfterNavReady(deeplink: string, origin: string) {
const isReady: boolean = yield select(selectIsMainNavigatorReady);
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.

We don't need to manage states to handle this trigger. Instead of yield select, we can use yield take(ACTION) so that it's triggered whenever the ACTION is dispatched.

Comment thread app/store/persistConfig/index.ts Outdated
{ whitelist: ['onboarding'] },
);

const persistNavigationTransform = createTransform<
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.

No need to manage navigation state. Checkout the comment in the saga file

// immediately; if in flight it awaits the shared promise. This guards the
// branches below that touch `SDKConnect.getInstance().state.*`.
try {
await SDKConnect.init({ context: 'deeplink' });
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.

Since SDKConnect is a service, it feels off to initialize it inside of this file, which is specific for handling deeplinks. From my understanding, SDKConnect initialization isn't dependent on deeplinks right?

Comment thread app/store/sagas/index.ts Outdated
@sonarqubecloud
Copy link
Copy Markdown

Cal-L
Cal-L previously approved these changes May 24, 2026
Copy link
Copy Markdown
Contributor

@Cal-L Cal-L left a comment

Choose a reason for hiding this comment

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

Lgtm

Copilot AI and others added 2 commits May 25, 2026 09:21
Agent-Logs-Url: https://github.com/MetaMask/metamask-mobile/sessions/6f9642f3-94a4-47b7-8559-684ad3a01fb6

Co-authored-by: baptiste-marchand <75846779+baptiste-marchand@users.noreply.github.com>
…deeplink and navigation/saga tests (#30585)

`ci` run `26232737527` failed in job `77197577872` on `yarn
format:check` due to Prettier drift in three TypeScript files. This PR
aligns those files with repository formatting rules only (no behavioral
changes intended).

- **Formatting alignment in deeplink utilities**
- Reformatted `app/core/DeeplinkManager/util/deeplinks/index.ts` to
match Prettier output for multiline call expressions.

- **Formatting alignment in navigation reducer tests**
- Reformatted `app/reducers/navigation/index.test.ts` for expected
wrapping of nested `expect(...)` expressions.

- **Formatting alignment in root saga tests**
- Reformatted `app/store/sagas/sagas.test.ts` for chained invocation
wrapping consistency.

Example of the kind of change applied:

```ts
// before
expect(navigationReducer(initialNavigationState, mainNavigatorReady())).toBe(
  initialNavigationState,
);

// after
expect(
  navigationReducer(initialNavigationState, mainNavigatorReady()),
).toBe(initialNavigationState);
```
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.

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.

Reviewed by Cursor Bugbot for commit 750a5a5. Configure here.

Comment thread app/store/sagas/index.ts
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeWalletPlatform, SmokeBrowser, SmokeSwap, SmokeConfirmations, SmokeMoney, SmokePerps, SmokePredictions, SmokeNetworkAbstractions
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: high
  • AI Confidence: 70%
click to see 🤖 AI reasoning details

E2E Test Selection:
The PR makes changes in the DeeplinkManager legacy handlers (handleMetaMaskDeeplink, handleUniversalLink), deepLinkAnalytics utilities and types, as well as navigation actions, route constants, MainNavigator, and root sagas. This directly affects how the app parses, routes, and tracks deep links and universal links, and how navigation is triggered from them.

Deep links are entry points into multiple critical user flows:

  • Swap and bridge (SmokeSwap) via trade-related links
  • Buy/Sell (SmokeMoney) via ramp deep links
  • Perps and Predictions via trade actions and Trending sections
  • Browser/dApp flows via external dApp links (SmokeBrowser)
  • General wallet screens and Trending sections (SmokeWalletPlatform)
  • Potential chain/network context changes when opening dApps or specific networks (SmokeNetworkAbstractions)
  • Many of the above flows trigger confirmations (SmokeConfirmations)

Because navigation layer (MainNavigator, Routes, navigation actions, sagas) is modified, regressions could impact screen transitions, parameter passing, and entry into transaction flows across the app. This is a high-risk, cross-cutting change affecting multiple verticals.

Tags selected ensure coverage of:

  • Core wallet + Trending entry points (SmokeWalletPlatform)
  • Browser deep link and dApp entry behavior (SmokeBrowser)
  • Swap flows (SmokeSwap) + required SmokeConfirmations
  • Fiat on/off-ramp flows (SmokeMoney)
  • Perps flows (SmokePerps) + WalletPlatform + Confirmations
  • Predictions flows (SmokePredictions) + WalletPlatform + Confirmations
  • Network selection and chain permission behavior potentially triggered by deep links (SmokeNetworkAbstractions)

Tags not selected (e.g., SmokeSnaps, SmokeSeedlessOnboarding, SmokeIdentity, SmokeAccounts, SmokeNetworkExpansion, SmokeMultiChainAPI) are not directly implicated by the changes, as no controller, snap, onboarding, sync, or multi-chain provider code was modified.

Given the breadth of navigation and deeplink changes, broad smoke coverage is warranted.

Performance Test Selection:
Changes are focused on deeplink parsing, analytics typing, navigation actions, and saga wiring. There are no modifications to rendering-heavy components, list views, asset loading, onboarding, login, swap quote logic, or startup/Engine initialization. Therefore, no direct performance impact is expected and performance test suites are not required.

View GitHub Actions results

@sonarqubecloud
Copy link
Copy Markdown

@baptiste-marchand baptiste-marchand removed the INVALID-PR-TEMPLATE PR's body doesn't match template label May 26, 2026
@metamaskbotv2 metamaskbotv2 Bot added INVALID-PR-TEMPLATE PR's body doesn't match template and removed INVALID-PR-TEMPLATE PR's body doesn't match template labels May 26, 2026
@baptiste-marchand baptiste-marchand added this pull request to the merge queue May 26, 2026
@github-project-automation github-project-automation Bot moved this from Has approvals, needs CODEOWNER to Review finalised - Ready to be merged in PR review queue May 26, 2026
Merged via the queue into main with commit 2318982 May 26, 2026
330 of 332 checks passed
@baptiste-marchand baptiste-marchand deleted the refactor/speed-up-deeplinks branch May 26, 2026 08:25
@github-actions github-actions Bot locked and limited conversation to collaborators May 26, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.80.0 Issue or pull request that will be included in release 7.80.0 label May 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.80.0 Issue or pull request that will be included in release 7.80.0 size-L team-engagement

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

7 participants