From a9917c3f43477b72dca4790f8fdef32cafc132c0 Mon Sep 17 00:00:00 2001 From: david0xd Date: Thu, 13 Nov 2025 16:07:33 +0100 Subject: [PATCH 1/8] Add documentation for deep links --- README.md | 1 + docs/deep-links.md | 192 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 docs/deep-links.md diff --git a/README.md b/README.md index c7920da9..03f38f30 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,4 @@ This is a living repository — nothing is set in stone! If you're member of Met - [UI Integration Testing Guidelines](./docs/testing/ui-integration-testing.md) - [E2E Testing Guidelines](./docs/testing/e2e-testing.md) - [TypeScript Guidelines](./docs/typescript.md) +- [Deep links Guidelines](./docs/deep-links.md) diff --git a/docs/deep-links.md b/docs/deep-links.md new file mode 100644 index 00000000..b4cfd9c0 --- /dev/null +++ b/docs/deep-links.md @@ -0,0 +1,192 @@ +# Deep links + +## Overview +MetaMask deep links are smart URLs that dynamically route users to the most relevant point in their journey—based +on their device, app status, and the action we want them to take. +Deep links help reduce friction, personalize the journey, and drive higher conversion. +For a link to be a deeplink, it needs to have logic baked in. + +### Example +For more clarity on how deep links work, here is an example of a deep link which links to a swaps feature MetaMask extension. +https://link.metamask.io/swap?sig=AWqgclBcX7wDKXJ-ZbABoRU2pzVS7xQAA5UsIuWEzKVchvqyYos_w0At4zR33_0wJdFAypIJM4VgboiU3ghhUQ + +- Clicking on the deep link above would have the following behavior: + - If a user is on DESKTOP: + - And MetaMask extension is installed: + - It will take the user to the Swap page. + - And MetaMask extension is not installed: + - It will take the user to the metamask.io/download website. + +### Additional notes +- If a deep link is not registered in the extension, the error "Not found" page will be displayed. + +### Deep link user flow + +```mermaid +--- +title: Examole user flow diagram for deep linking to swaps page +displayMode: compact +--- +graph TD + A[Click on a Deep Link for Swaps] -->|User on DESKTOP| B{MetaMask Extension Installed?} + B -->|Yes| C{Is the Link Signed?} + B -->|No| D[Take User to metamask.io/download] + + C -->|Yes| E[Skip Interstitial Warning Page\n and show link info page once] + C -->|No| F[Show Interstitial Warning Page] + + E --> G[Take User to Swap Page] + F -->|User Confirms| G +``` + +## Creating and signing deep links +There are several steps that need to be taken to register a deep link in the MetaMask extension. + +#### 1. Construct a deep link and sign it. +For deep link construction we use `branch.io` features. +Branch service is configured and automatically capable of linking any path to the extension route, if the route is registered. +So, adding `/home` or `/swap` at the end of `link.metamask.io` would be enough, and there is no need for additional configurations on the Branch side. + +Internal deep links are supposed to be signed and skip interstitial (warning page). +Links are signed with a protected dedicated link signer, so additional access is required (ask internally for link signer application and access). + +Example: +```text +https://link.metamask.io/home?sig=mmVgbZk8ucIY8-syEWrtnRvZlrHGQgM7Jl26fgRVjuwX62UVvhUE5nxOCWn0kEbYhZ_P17nwCZBbqJU7rPMx2w +``` + +#### 2. Register deep link route in extension + +Add a new file to the deep links route folder with a name of the route (e.g., home.ts) and create route definition. +```typescript +import { DEFAULT_ROUTE, Route } from './route'; + +export default new Route({ + pathname: '/home', + getTitle: (_: URLSearchParams) => 'deepLink_theHomePage', + handler: function handler(_: URLSearchParams) { + return { path: DEFAULT_ROUTE, query: new URLSearchParams() }; + }, +}); + +``` +Directory location: https://github.com/MetaMask/metamask-extension/tree/main/shared/lib/deep-links/routes + +Add new route by calling `addRoute` utility function in index.ts file of the deep links route folder. +```typescript +import home from './home'; + +addRoute(home); +``` + +#### Additional notes +- Deep link route definition consists of: + - `pathname` - Route path identifier (e.g., `/home`, `/swaps`, `/notifications`) + - `getTitle` - Callback function that should return title of the deep link page. This is usually represented by the translation constant (e.g., `deepLink_theHomePage`). + - `handler` - Callback function that should return an object with `path` and `query` properties. + - `path` - Exact path of the route used in extension. Taken from the existing routes definitions (e.g., `DEFAULT_ROUTE`, `NOTIFICATIONS_ROUTE`). + - `query` - URL query params. Constructed using `URLSearchParams` constructor function. +- Make sure that the route exists and return it from a handler under `path` key. +- Handler function can transform query parameters if necessary. +- For a deeper understanding of the route definitions and their properties, [check router implementation and its types](https://github.com/MetaMask/metamask-extension/blob/main/shared/lib/deep-links/routes/route.ts#L18). + +## Architecture +For a deeper understanding of the deep links implementation and its architecture, here is the abstracted class diagram +that shows associations and responsibilities of the particular components in the system. + +```mermaid +classDiagram + class DeepLinkRouter { + -getExtensionURL: Function + -getState: Function + +install() + +uninstall() + -handleBeforeRequest() + -tryNavigateTo() + -canSkipInterstitial() + -formatUrlForInterstitialPage() + -get404ErrorURL() + -redirectTab() + } + + class DeepLink { + -description: string + -extraDescription: string + -route: Route + -title: string + -cta: string + -skipDeepLinkInterstitialChecked: boolean + -isLoading: boolean + +render() + -updateStateFromUrl() + -onRemindMeStateChanged() + } + + class Route { + +pathname: string + +getTitle: Function + +handler: Function + +constructor(options: RouteOptions) + } + + class ParsedDeepLink { + +route: Route + +destination: Destination + +signature: SignatureStatus + } + + class Destination { + +path: string + +query: URLSearchParams + OR + +redirectTo: URL + } + + class EventEmitter { + +emit() + +on() + } + + class RouteRegistry { + +routes: Map + +addRoute(route: Route) + } + + %% Route implementations + class HomeRoute { + +pathname: "/home" + +getTitle() + +handler() + } + + class SwapRoute { + +pathname: "/swap" + +getTitle() + +handler() + } + + class NotificationsRoute { + +pathname: "/notifications" + +getTitle() + +handler() + } + + %% Define relationships + EventEmitter <|-- DeepLinkRouter : extends + DeepLinkRouter --> ParsedDeepLink : parses and creates + DeepLinkRouter --> Route : uses routes from + DeepLink --> ParsedDeepLink : displays and processes + DeepLink --> Route : links to route + ParsedDeepLink --> Route : contains + ParsedDeepLink --> Destination : contains + Route <|-- HomeRoute : implements + Route <|-- SwapRoute : implements + Route <|-- NotificationsRoute : implements + RouteRegistry --> Route : registers and stores +``` + +Deep Link Router is instantiated in service worker ([background.js](https://github.com/MetaMask/metamask-extension/blob/main/app/scripts/background.js#L757)). + +## Security +Internal deep links are supposed to be signed and signed links will skip interstitial (warning page). +By signing links, it is ensured that there is a distinction between what MetaMask provides and what is coming from a community outside. From 2866740c47558e9434c3dc23acdd6d949fef569f Mon Sep 17 00:00:00 2001 From: david0xd Date: Thu, 13 Nov 2025 16:47:37 +0100 Subject: [PATCH 2/8] Fix lint --- docs/deep-links.md | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/docs/deep-links.md b/docs/deep-links.md index b4cfd9c0..1a394957 100644 --- a/docs/deep-links.md +++ b/docs/deep-links.md @@ -1,23 +1,26 @@ # Deep links ## Overview -MetaMask deep links are smart URLs that dynamically route users to the most relevant point in their journey—based -on their device, app status, and the action we want them to take. -Deep links help reduce friction, personalize the journey, and drive higher conversion. + +MetaMask deep links are smart URLs that dynamically route users to the most relevant point in their journey—based +on their device, app status, and the action we want them to take. +Deep links help reduce friction, personalize the journey, and drive higher conversion. For a link to be a deeplink, it needs to have logic baked in. ### Example + For more clarity on how deep links work, here is an example of a deep link which links to a swaps feature MetaMask extension. https://link.metamask.io/swap?sig=AWqgclBcX7wDKXJ-ZbABoRU2pzVS7xQAA5UsIuWEzKVchvqyYos_w0At4zR33_0wJdFAypIJM4VgboiU3ghhUQ - Clicking on the deep link above would have the following behavior: - If a user is on DESKTOP: - - And MetaMask extension is installed: + - And MetaMask extension is installed: - It will take the user to the Swap page. - - And MetaMask extension is not installed: + - And MetaMask extension is not installed: - It will take the user to the metamask.io/download website. ### Additional notes + - If a deep link is not registered in the extension, the error "Not found" page will be displayed. ### Deep link user flow @@ -40,24 +43,28 @@ graph TD ``` ## Creating and signing deep links + There are several steps that need to be taken to register a deep link in the MetaMask extension. #### 1. Construct a deep link and sign it. -For deep link construction we use `branch.io` features. + +For deep link construction we use `branch.io` features. Branch service is configured and automatically capable of linking any path to the extension route, if the route is registered. -So, adding `/home` or `/swap` at the end of `link.metamask.io` would be enough, and there is no need for additional configurations on the Branch side. +So, adding `/home` or `/swap` at the end of `link.metamask.io` would be enough, and there is no need for additional configurations on the Branch side. -Internal deep links are supposed to be signed and skip interstitial (warning page). +Internal deep links are supposed to be signed and skip interstitial (warning page). Links are signed with a protected dedicated link signer, so additional access is required (ask internally for link signer application and access). Example: + ```text -https://link.metamask.io/home?sig=mmVgbZk8ucIY8-syEWrtnRvZlrHGQgM7Jl26fgRVjuwX62UVvhUE5nxOCWn0kEbYhZ_P17nwCZBbqJU7rPMx2w +https://link.metamask.io/home?sig=mmVgbZk8ucIY8-syEWrtnRvZlrHGQgM7Jl26fgRVjuwX62UVvhUE5nxOCWn0kEbYhZ_P17nwCZBbqJU7rPMx2w ``` #### 2. Register deep link route in extension Add a new file to the deep links route folder with a name of the route (e.g., home.ts) and create route definition. + ```typescript import { DEFAULT_ROUTE, Route } from './route'; @@ -68,11 +75,12 @@ export default new Route({ return { path: DEFAULT_ROUTE, query: new URLSearchParams() }; }, }); - ``` + Directory location: https://github.com/MetaMask/metamask-extension/tree/main/shared/lib/deep-links/routes Add new route by calling `addRoute` utility function in index.ts file of the deep links route folder. + ```typescript import home from './home'; @@ -80,6 +88,7 @@ addRoute(home); ``` #### Additional notes + - Deep link route definition consists of: - `pathname` - Route path identifier (e.g., `/home`, `/swaps`, `/notifications`) - `getTitle` - Callback function that should return title of the deep link page. This is usually represented by the translation constant (e.g., `deepLink_theHomePage`). @@ -91,7 +100,8 @@ addRoute(home); - For a deeper understanding of the route definitions and their properties, [check router implementation and its types](https://github.com/MetaMask/metamask-extension/blob/main/shared/lib/deep-links/routes/route.ts#L18). ## Architecture -For a deeper understanding of the deep links implementation and its architecture, here is the abstracted class diagram + +For a deeper understanding of the deep links implementation and its architecture, here is the abstracted class diagram that shows associations and responsibilities of the particular components in the system. ```mermaid @@ -185,8 +195,9 @@ classDiagram RouteRegistry --> Route : registers and stores ``` -Deep Link Router is instantiated in service worker ([background.js](https://github.com/MetaMask/metamask-extension/blob/main/app/scripts/background.js#L757)). +Deep Link Router is instantiated in service worker ([background.js](https://github.com/MetaMask/metamask-extension/blob/main/app/scripts/background.js#L757)). ## Security + Internal deep links are supposed to be signed and signed links will skip interstitial (warning page). By signing links, it is ensured that there is a distinction between what MetaMask provides and what is coming from a community outside. From b5ac34a597d4a9167b7dffc22063d375834a551a Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 17 Nov 2025 17:38:36 +0100 Subject: [PATCH 3/8] Add some documentation changes --- docs/deep-links.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/deep-links.md b/docs/deep-links.md index 1a394957..acb08a8c 100644 --- a/docs/deep-links.md +++ b/docs/deep-links.md @@ -27,7 +27,7 @@ https://link.metamask.io/swap?sig=AWqgclBcX7wDKXJ-ZbABoRU2pzVS7xQAA5UsIuWEzKVchv ```mermaid --- -title: Examole user flow diagram for deep linking to swaps page +title: Example user flow diagram for deep linking to swaps page displayMode: compact --- graph TD @@ -35,10 +35,10 @@ graph TD B -->|Yes| C{Is the Link Signed?} B -->|No| D[Take User to metamask.io/download] - C -->|Yes| E[Skip Interstitial Warning Page\n and show link info page once] - C -->|No| F[Show Interstitial Warning Page] + C -->|Yes| E[Show interstitial page with link information] + C -->|No| F[Show interstitial page with warning] - E --> G[Take User to Swap Page] + E --> |User Confirms| G[Take User to Swap Page] F -->|User Confirms| G ``` @@ -49,10 +49,10 @@ There are several steps that need to be taken to register a deep link in the Met #### 1. Construct a deep link and sign it. For deep link construction we use `branch.io` features. -Branch service is configured and automatically capable of linking any path to the extension route, if the route is registered. +Branch service is configured and automatically capable of linking to any path registered within the extension as a deep link route. So, adding `/home` or `/swap` at the end of `link.metamask.io` would be enough, and there is no need for additional configurations on the Branch side. -Internal deep links are supposed to be signed and skip interstitial (warning page). +Signed deep links will skip the interstitial warning page and show the link information interstitial instead. Links are signed with a protected dedicated link signer, so additional access is required (ask internally for link signer application and access). Example: @@ -70,7 +70,7 @@ import { DEFAULT_ROUTE, Route } from './route'; export default new Route({ pathname: '/home', - getTitle: (_: URLSearchParams) => 'deepLink_theHomePage', + getTitle: (_: URLSearchParams) => 'deepLink_theHomePage', // deepLink_theHomePage is a translation constant from messages.json handler: function handler(_: URLSearchParams) { return { path: DEFAULT_ROUTE, query: new URLSearchParams() }; }, @@ -91,12 +91,14 @@ addRoute(home); - Deep link route definition consists of: - `pathname` - Route path identifier (e.g., `/home`, `/swaps`, `/notifications`) - - `getTitle` - Callback function that should return title of the deep link page. This is usually represented by the translation constant (e.g., `deepLink_theHomePage`). + - `getTitle` - Callback function that should return title of the deep link page. This is represented by the translation constant (e.g., `deepLink_theHomePage` from `messages.json`). - `handler` - Callback function that should return an object with `path` and `query` properties. - `path` - Exact path of the route used in extension. Taken from the existing routes definitions (e.g., `DEFAULT_ROUTE`, `NOTIFICATIONS_ROUTE`). - `query` - URL query params. Constructed using `URLSearchParams` constructor function. - Make sure that the route exists and return it from a handler under `path` key. - Handler function can transform query parameters if necessary. + - Handler function must be synchronous. + - Handler function must not change anything. Deep links are read-only, and all actions/changes must be confirmed by the user in the UI layer. - For a deeper understanding of the route definitions and their properties, [check router implementation and its types](https://github.com/MetaMask/metamask-extension/blob/main/shared/lib/deep-links/routes/route.ts#L18). ## Architecture @@ -195,9 +197,11 @@ classDiagram RouteRegistry --> Route : registers and stores ``` -Deep Link Router is instantiated in service worker ([background.js](https://github.com/MetaMask/metamask-extension/blob/main/app/scripts/background.js#L757)). +Deep Link Router is instantiated in the background script ([background.js](https://github.com/MetaMask/metamask-extension/blob/main/app/scripts/background.js#L757)). ## Security -Internal deep links are supposed to be signed and signed links will skip interstitial (warning page). +It is recommended that deep links provided by MetaMask are signed. +Signed links will skip the interstitial warning page and show the link information interstitial instead. +Link information interstitial can be skipped next time if a user checks the "Don't remind me again" option. By signing links, it is ensured that there is a distinction between what MetaMask provides and what is coming from a community outside. From 6fb35d77b0dc52f56e9d797f9240de74ed278b60 Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 17 Nov 2025 17:46:15 +0100 Subject: [PATCH 4/8] Fix lint --- docs/deep-links.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deep-links.md b/docs/deep-links.md index acb08a8c..ac38b949 100644 --- a/docs/deep-links.md +++ b/docs/deep-links.md @@ -201,7 +201,7 @@ Deep Link Router is instantiated in the background script ([background.js](https ## Security -It is recommended that deep links provided by MetaMask are signed. +It is recommended that deep links provided by MetaMask are signed. Signed links will skip the interstitial warning page and show the link information interstitial instead. Link information interstitial can be skipped next time if a user checks the "Don't remind me again" option. By signing links, it is ensured that there is a distinction between what MetaMask provides and what is coming from a community outside. From 4dc41f4c7d265ee21e3aab70db1cac429c8a2250 Mon Sep 17 00:00:00 2001 From: david0xd Date: Fri, 21 Nov 2025 15:22:55 +0100 Subject: [PATCH 5/8] Add a link to the ADR, as suggested --- docs/deep-links.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/deep-links.md b/docs/deep-links.md index ac38b949..143bbdd4 100644 --- a/docs/deep-links.md +++ b/docs/deep-links.md @@ -52,7 +52,9 @@ For deep link construction we use `branch.io` features. Branch service is configured and automatically capable of linking to any path registered within the extension as a deep link route. So, adding `/home` or `/swap` at the end of `link.metamask.io` would be enough, and there is no need for additional configurations on the Branch side. -Signed deep links will skip the interstitial warning page and show the link information interstitial instead. +Signed deep links will skip the interstitial warning page and show the link information interstitial instead. +For more information, please refer to the [ADR](https://github.com/MetaMask/decisions/blob/dcb42a5395507928e87c183dd1809c83d9cb408d/decisions/core/0007-deep-linking-into-wallet.md). + Links are signed with a protected dedicated link signer, so additional access is required (ask internally for link signer application and access). Example: From bdb1491f58eb5b720caf5d7b12d244f685bbccfe Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 24 Nov 2025 10:03:59 +0100 Subject: [PATCH 6/8] Fix lint --- docs/deep-links.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deep-links.md b/docs/deep-links.md index 143bbdd4..d73d1d28 100644 --- a/docs/deep-links.md +++ b/docs/deep-links.md @@ -52,7 +52,7 @@ For deep link construction we use `branch.io` features. Branch service is configured and automatically capable of linking to any path registered within the extension as a deep link route. So, adding `/home` or `/swap` at the end of `link.metamask.io` would be enough, and there is no need for additional configurations on the Branch side. -Signed deep links will skip the interstitial warning page and show the link information interstitial instead. +Signed deep links will skip the interstitial warning page and show the link information interstitial instead. For more information, please refer to the [ADR](https://github.com/MetaMask/decisions/blob/dcb42a5395507928e87c183dd1809c83d9cb408d/decisions/core/0007-deep-linking-into-wallet.md). Links are signed with a protected dedicated link signer, so additional access is required (ask internally for link signer application and access). From 726593b061d3d51b6d407472b82e3ef504f717ad Mon Sep 17 00:00:00 2001 From: david0xd Date: Tue, 16 Dec 2025 23:37:22 +0100 Subject: [PATCH 7/8] Add deferred deep link documentation --- docs/deep-links.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/deep-links.md b/docs/deep-links.md index d73d1d28..377bdb4f 100644 --- a/docs/deep-links.md +++ b/docs/deep-links.md @@ -102,6 +102,7 @@ addRoute(home); - Handler function must be synchronous. - Handler function must not change anything. Deep links are read-only, and all actions/changes must be confirmed by the user in the UI layer. - For a deeper understanding of the route definitions and their properties, [check router implementation and its types](https://github.com/MetaMask/metamask-extension/blob/main/shared/lib/deep-links/routes/route.ts#L18). +- For a link to work as a deferred deep link, it needs to be added to the branch.io _LinkHub_. For more information check [deferred deep links section below](#deferred-deep-links). ## Architecture @@ -201,6 +202,22 @@ classDiagram Deep Link Router is instantiated in the background script ([background.js](https://github.com/MetaMask/metamask-extension/blob/main/app/scripts/background.js#L757)). +### Deferred deep links + +Deferred deep links are used to navigate a user to a deep link route after the extension is installed. +For deferred deep link to work, it is required to add the specific link with its path to the branch.io _LinkHub_. + +The extension receives deferred deep link data from the MetaMask website, which collects that data via the integrated Branch.io SDK. + +Deferred deep link flow: +1. When a user lands on the MetaMask website download page, the website stores the deferred deep link inside cookie. +2. When the user installs MetaMask extension, the extension reads and stores the cookie from the MetaMask website. +3. Then extension navigates the user to the deferred deep link after successful onboarding. + +Notes: +- Deferred deep link is stored in the extension's local storage immediately after installation. +- User is navigated to the deferred deep link only if they completed the onboarding process within the two hours after installation. + ## Security It is recommended that deep links provided by MetaMask are signed. From c6fe673af60e804b3b90988d819dea6e164024fa Mon Sep 17 00:00:00 2001 From: david0xd Date: Tue, 16 Dec 2025 23:41:58 +0100 Subject: [PATCH 8/8] Fix lint --- docs/deep-links.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/deep-links.md b/docs/deep-links.md index 377bdb4f..0f5eed31 100644 --- a/docs/deep-links.md +++ b/docs/deep-links.md @@ -204,17 +204,19 @@ Deep Link Router is instantiated in the background script ([background.js](https ### Deferred deep links -Deferred deep links are used to navigate a user to a deep link route after the extension is installed. +Deferred deep links are used to navigate a user to a deep link route after the extension is installed. For deferred deep link to work, it is required to add the specific link with its path to the branch.io _LinkHub_. The extension receives deferred deep link data from the MetaMask website, which collects that data via the integrated Branch.io SDK. Deferred deep link flow: + 1. When a user lands on the MetaMask website download page, the website stores the deferred deep link inside cookie. 2. When the user installs MetaMask extension, the extension reads and stores the cookie from the MetaMask website. 3. Then extension navigates the user to the deferred deep link after successful onboarding. Notes: + - Deferred deep link is stored in the extension's local storage immediately after installation. - User is navigated to the deferred deep link only if they completed the onboarding process within the two hours after installation.