Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/core/AppConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export default {
},
},
MM_UNIVERSAL_LINK_HOST: 'metamask.app.link',
MM_UNIVERSAL_LINK_HOST_ALTERNATE: 'metamask-alternate.app.link',
MM_IO_UNIVERSAL_LINK_HOST: 'link.metamask.io',
MM_IO_UNIVERSAL_LINK_TEST_HOST: 'link-test.metamask.io',
MM_DEEP_ITMS_APP_LINK: 'https://metamask.app.link/skAH3BaF99',
Expand Down
123 changes: 122 additions & 1 deletion app/core/DeeplinkManager/DeeplinkManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { NavigationProp, ParamListBase } from '@react-navigation/native';
import { waitFor } from '@testing-library/react-native';
import FCMService from '../../util/notifications/services/FCMService';
import NavigationService from '../NavigationService';
import SharedDeeplinkManager, { DeeplinkManager } from './DeeplinkManager';
import SharedDeeplinkManager, {
DeeplinkManager,
rewriteBranchUri,
} from './DeeplinkManager';
import type { BranchParams } from './types/deepLinkAnalytics.types';
import { handleDeeplink } from './handlers/legacy/handleDeeplink';
import switchNetwork from '../../util/networks/switchNetwork';
import parseDeeplink from './utils/parseDeeplink';
Expand Down Expand Up @@ -282,6 +286,34 @@ describe('SharedDeeplinkManager', () => {
});
});

describe('rewriteBranchUri', () => {
it('rewrites host and path to link.metamask.io and preserves query when +clicked_branch_link and $deeplink_path are set', () => {
const uri =
'https://metamask-alternate.app.link/1WkF6GmE40b?amount=100&from=0x';
const params: BranchParams = {
'+clicked_branch_link': true,
$deeplink_path: 'swap',
};
expect(rewriteBranchUri(uri, params)).toBe(
'https://link.metamask.io/swap?amount=100&from=0x',
);
});

it('returns uri unchanged when +clicked_branch_link is false', () => {
const uri = 'https://metamask.app.link/swap';
expect(
rewriteBranchUri(uri, { '+clicked_branch_link': false } as BranchParams),
).toBe(uri);
});

it('returns uri unchanged when $deeplink_path is missing', () => {
const uri = 'https://metamask.app.link/swap';
expect(
rewriteBranchUri(uri, { '+clicked_branch_link': true } as BranchParams),
).toBe(uri);
});
});

describe('DeeplinkManager.start Branch deeplink handling', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -304,6 +336,31 @@ describe('DeeplinkManager.start Branch deeplink handling', () => {
expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockDeeplink });
});

it('rewrites cold start Branch link using $deeplink_path from getLatestReferringParams', async () => {
(branch.getLatestReferringParams as jest.Mock).mockResolvedValue({
'+clicked_branch_link': true,
$deeplink_path: 'swap',
'~referring_link':
'https://metamask-alternate.app.link/1WkF6GmE40b?amount=500',
});
DeeplinkManager.start();
await new Promise((resolve) => setImmediate(resolve));
expect(handleDeeplink).toHaveBeenCalledWith({
uri: 'https://link.metamask.io/swap?amount=500',
});
});

it('falls back to +non_branch_link on cold start when +clicked_branch_link is false', async () => {
const mockDeeplink = 'https://link.metamask.io/home';
(branch.getLatestReferringParams as jest.Mock).mockResolvedValue({
'+clicked_branch_link': false,
'+non_branch_link': mockDeeplink,
});
DeeplinkManager.start();
await new Promise((resolve) => setImmediate(resolve));
expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockDeeplink });
});

it('subscribes to Branch deeplink events', async () => {
DeeplinkManager.start();
expect(branch.subscribe).toHaveBeenCalled();
Expand All @@ -318,4 +375,68 @@ describe('DeeplinkManager.start Branch deeplink handling', () => {
await new Promise((resolve) => setImmediate(resolve));
expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockUri });
});

it('rewrites Branch short link to link.metamask.io when +clicked_branch_link and $deeplink_path are present', async () => {
DeeplinkManager.start();
const callback = (branch.subscribe as jest.Mock).mock.calls[0][0];

callback({
uri: 'https://metamask-alternate.app.link/1WkF6GmE40b?amount=1000000&from=eip155%3A1%2Ferc20%3A0xabc',
params: {
'+clicked_branch_link': true,
$deeplink_path: 'swap',
},
});

await new Promise((resolve) => setImmediate(resolve));
expect(handleDeeplink).toHaveBeenCalledWith({
uri: 'https://link.metamask.io/swap?amount=1000000&from=eip155%3A1%2Ferc20%3A0xabc',
});
});

it('passes URI through unchanged when +clicked_branch_link is false', async () => {
DeeplinkManager.start();
const callback = (branch.subscribe as jest.Mock).mock.calls[0][0];
const mockUri = 'https://metamask.app.link/swap?amount=100';

callback({
uri: mockUri,
params: { '+clicked_branch_link': false },
});

await new Promise((resolve) => setImmediate(resolve));
expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockUri });
});

it('passes URI through unchanged when $deeplink_path is missing', async () => {
DeeplinkManager.start();
const callback = (branch.subscribe as jest.Mock).mock.calls[0][0];
const mockUri = 'https://metamask.app.link/swap?amount=100';

callback({
uri: mockUri,
params: { '+clicked_branch_link': true },
});

await new Promise((resolve) => setImmediate(resolve));
expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockUri });
});

it('strips leading slash from $deeplink_path when rewriting', async () => {
DeeplinkManager.start();
const callback = (branch.subscribe as jest.Mock).mock.calls[0][0];

callback({
uri: 'https://metamask-alternate.app.link/ABC123',
params: {
'+clicked_branch_link': true,
$deeplink_path: '/swap/token',
},
});

await new Promise((resolve) => setImmediate(resolve));
expect(handleDeeplink).toHaveBeenCalledWith({
uri: 'https://link.metamask.io/swap/token',
});
});
});
49 changes: 43 additions & 6 deletions app/core/DeeplinkManager/DeeplinkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ import Logger from '../../util/Logger';
import { handleDeeplink } from './handlers/legacy/handleDeeplink';
import FCMService from '../../util/notifications/services/FCMService';
import AppConstants from '../AppConstants';
import { BranchParams } from './types/deepLinkAnalytics.types';

/**
* When Branch resolves a short link (e.g. metamask-alternate.app.link/1WkF6GmE40b),
* the URI path may be link ID, not an in-app route. If the resolved params indicate
* a clicked Branch link with a $deeplink_path, replace the host and path segment
* with link.metamask.io/$deeplink_path while preserving the original query string.
*/
export function rewriteBranchUri(
uri: string | undefined,
params: BranchParams | undefined,
): string | undefined {
try {
if (!uri || !params?.['+clicked_branch_link']) return uri;
const rawPath = params.$deeplink_path;
if (typeof rawPath !== 'string') return uri;

const parsed = new URL(uri);
parsed.host = AppConstants.MM_IO_UNIVERSAL_LINK_HOST;
// Set the pathname to the sanitized $deeplink_path
parsed.pathname = `/${rawPath.replace(/^\//, '')}`;
return parsed.toString();
} catch (error) {
Logger.error(error as Error, `Error rewriting Branch URI: ${uri}`);
return uri;
}
}

export class DeeplinkManager {
// singleton instance
Expand Down Expand Up @@ -66,6 +93,17 @@ export class DeeplinkManager {

try {
const latestParams = await branch.getLatestReferringParams();

// Cold start: params may contain a resolved Branch link with $deeplink_path.
const rewritten = rewriteBranchUri(
latestParams?.['~referring_link'] as string | undefined,
latestParams as Record<string, unknown> | undefined,
);
if (rewritten) {
handleDeeplink({ uri: rewritten });
return;
}

const deeplink = latestParams?.['+non_branch_link'] as string;
if (deeplink) {
handleDeeplink({ uri: deeplink });
Expand Down Expand Up @@ -117,12 +155,11 @@ export class DeeplinkManager {
const branchError = new Error(error);
Logger.error(branchError, 'Error subscribing to branch.');
}
getBranchDeeplink(opts.uri);
//TODO: that async call in the subscribe doesn't look good to me
branch.getLatestReferringParams().then((val) => {
const deeplink = opts.uri || (val['+non_branch_link'] as string);
handleDeeplink({ uri: deeplink });
});
const rewritten = rewriteBranchUri(
opts.uri,
opts.params as Record<string, unknown> | undefined,
);
getBranchDeeplink(rewritten ?? opts.uri);
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import Logger from '../../../../util/Logger';

const {
MM_UNIVERSAL_LINK_HOST,
MM_UNIVERSAL_LINK_HOST_ALTERNATE,
MM_IO_UNIVERSAL_LINK_HOST,
MM_IO_UNIVERSAL_LINK_TEST_HOST,
} = AppConstants;
Expand Down Expand Up @@ -216,6 +217,7 @@ async function handleUniversalLink({

const isSupportedDomain =
urlObj.hostname === MM_UNIVERSAL_LINK_HOST ||
urlObj.hostname === MM_UNIVERSAL_LINK_HOST_ALTERNATE ||
urlObj.hostname === MM_IO_UNIVERSAL_LINK_HOST ||
urlObj.hostname === MM_IO_UNIVERSAL_LINK_TEST_HOST;

Expand Down
2 changes: 2 additions & 0 deletions app/core/DeeplinkManager/util/deeplinks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import AppConstants from '../../../AppConstants';

const {
MM_UNIVERSAL_LINK_HOST,
MM_UNIVERSAL_LINK_HOST_ALTERNATE,
MM_IO_UNIVERSAL_LINK_HOST,
MM_IO_UNIVERSAL_LINK_TEST_HOST,
} = AppConstants;
Expand All @@ -10,6 +11,7 @@ const METAMASK_HOSTS = [
...new Set(
[
MM_UNIVERSAL_LINK_HOST || 'link.metamask.io',
MM_UNIVERSAL_LINK_HOST_ALTERNATE || 'metamask-alternate.app.link',
Comment thread
baptiste-marchand marked this conversation as resolved.
MM_IO_UNIVERSAL_LINK_HOST || 'link.metamask.io',
MM_IO_UNIVERSAL_LINK_TEST_HOST || 'link-test.metamask.io',
'metamask.app.link',
Expand Down
Loading