From 9e073ca3c7555d3667b9b79b90bf754c12561bcd Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Wed, 18 Mar 2026 18:52:52 +0000 Subject: [PATCH 01/23] feat: initial fixes for deeplinking --- .../DeeplinkManager/DeeplinkManager.test.ts | 131 +++++++++++++++--- app/core/DeeplinkManager/DeeplinkManager.ts | 55 ++++++-- .../__tests__/handleUniversalLink.test.ts | 107 ++------------ .../handlers/legacy/handleUniversalLink.ts | 37 +---- .../DeeplinkManager/util/deeplinks/index.ts | 18 ++- 5 files changed, 171 insertions(+), 177 deletions(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.test.ts b/app/core/DeeplinkManager/DeeplinkManager.test.ts index f77f417a66f9..a66c63eae44e 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.test.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.test.ts @@ -5,6 +5,7 @@ import NavigationService from '../NavigationService'; import SharedDeeplinkManager, { DeeplinkManager, rewriteBranchUri, + isBranchDomainUrl, } from './DeeplinkManager'; import type { BranchParams } from './types/deepLinkAnalytics.types'; import { handleDeeplink } from './handlers/legacy/handleDeeplink'; @@ -299,18 +300,33 @@ describe('rewriteBranchUri', () => { ); }); - it('returns uri unchanged when +clicked_branch_link is false', () => { + it('returns undefined when +clicked_branch_link is false', () => { const uri = 'https://metamask.app.link/swap'; expect( rewriteBranchUri(uri, { '+clicked_branch_link': false } as BranchParams), - ).toBe(uri); + ).toBeUndefined(); }); - it('returns uri unchanged when $deeplink_path is missing', () => { + it('returns undefined when $deeplink_path is missing', () => { const uri = 'https://metamask.app.link/swap'; expect( rewriteBranchUri(uri, { '+clicked_branch_link': true } as BranchParams), - ).toBe(uri); + ).toBeUndefined(); + }); + + it('returns undefined when uri is undefined', () => { + expect( + rewriteBranchUri(undefined, { + '+clicked_branch_link': true, + $deeplink_path: 'swap', + } as BranchParams), + ).toBeUndefined(); + }); + + it('returns undefined when params is undefined', () => { + expect( + rewriteBranchUri('https://metamask.app.link/swap', undefined), + ).toBeUndefined(); }); }); @@ -326,14 +342,14 @@ describe('DeeplinkManager.start Branch deeplink handling', () => { expect(branch.getLatestReferringParams).toHaveBeenCalledTimes(1); }); - it('processes cold start deeplink when non-branch link is found', async () => { - const mockDeeplink = 'https://link.metamask.io/home'; + it('does not process cold start deeplink when no rewrite is possible', async () => { (branch.getLatestReferringParams as jest.Mock).mockResolvedValue({ - '+non_branch_link': mockDeeplink, + '~referring_link': 'https://metamask-alternate.app.link/abc123', + '+clicked_branch_link': false, }); DeeplinkManager.start(); await new Promise((resolve) => setImmediate(resolve)); - expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockDeeplink }); + expect(handleDeeplink).not.toHaveBeenCalled(); }); it('rewrites cold start Branch link using $deeplink_path from getLatestReferringParams', async () => { @@ -350,17 +366,6 @@ describe('DeeplinkManager.start Branch deeplink handling', () => { }); }); - 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(); @@ -440,3 +445,91 @@ describe('DeeplinkManager.start Branch deeplink handling', () => { }); }); }); + +describe('isBranchDomainUrl', () => { + it('returns true for metamask.app.link URLs', () => { + expect(isBranchDomainUrl('https://metamask.app.link/abc123')).toBe(true); + }); + + it('returns true for metamask-alternate.app.link URLs', () => { + expect( + isBranchDomainUrl('https://metamask-alternate.app.link/abc123'), + ).toBe(true); + }); + + it('returns false for link.metamask.io URLs', () => { + expect(isBranchDomainUrl('https://link.metamask.io/swap')).toBe(false); + }); + + it('returns false for link-test.metamask.io URLs', () => { + expect(isBranchDomainUrl('https://link-test.metamask.io/buy')).toBe(false); + }); + + it('returns false for metamask:// custom scheme', () => { + expect(isBranchDomainUrl('metamask://swap')).toBe(false); + }); + + it('returns false for invalid URLs', () => { + expect(isBranchDomainUrl('not-a-url')).toBe(false); + }); +}); + +describe('DeeplinkManager.start Linking API filters Branch domain URLs', () => { + let mockGetInitialURL: jest.Mock; + let mockAddEventListener: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + (branch.getLatestReferringParams as jest.Mock).mockResolvedValue({}); + + const { Linking } = jest.requireMock('react-native'); + mockGetInitialURL = Linking.getInitialURL as jest.Mock; + mockAddEventListener = Linking.addEventListener as jest.Mock; + }); + + it('skips Branch domain URLs from Linking.getInitialURL', async () => { + mockGetInitialURL.mockResolvedValue('https://metamask.app.link/abc123'); + + DeeplinkManager.start(); + await new Promise((resolve) => setImmediate(resolve)); + + expect(handleDeeplink).not.toHaveBeenCalled(); + }); + + it('processes non-Branch URLs from Linking.getInitialURL', async () => { + mockGetInitialURL.mockResolvedValue( + 'https://link.metamask.io/swap?from=ETH', + ); + + DeeplinkManager.start(); + await new Promise((resolve) => setImmediate(resolve)); + + expect(handleDeeplink).toHaveBeenCalledWith({ + uri: 'https://link.metamask.io/swap?from=ETH', + }); + }); + + it('skips Branch domain URLs from Linking.addEventListener', () => { + DeeplinkManager.start(); + + const urlCallback = mockAddEventListener.mock.calls.find( + (call: unknown[]) => call[0] === 'url', + )?.[1]; + expect(urlCallback).toBeDefined(); + + urlCallback({ url: 'https://metamask-alternate.app.link/xyz' }); + expect(handleDeeplink).not.toHaveBeenCalled(); + }); + + it('processes custom scheme URLs from Linking.addEventListener', () => { + DeeplinkManager.start(); + + const urlCallback = mockAddEventListener.mock.calls.find( + (call: unknown[]) => call[0] === 'url', + )?.[1]; + expect(urlCallback).toBeDefined(); + + urlCallback({ url: 'metamask://buy' }); + expect(handleDeeplink).toHaveBeenCalledWith({ uri: 'metamask://buy' }); + }); +}); diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index 1c145b39b6d0..516017419401 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -9,6 +9,25 @@ import FCMService from '../../util/notifications/services/FCMService'; import AppConstants from '../AppConstants'; import { BranchParams } from './types/deepLinkAnalytics.types'; +const BRANCH_DOMAIN_HOSTS = [ + AppConstants.MM_UNIVERSAL_LINK_HOST, + AppConstants.MM_UNIVERSAL_LINK_HOST_ALTERNATE, +]; + +/** + * Branch domain URLs (metamask.app.link, metamask-alternate.app.link) are handled + * by the Branch SDK. Returns true if the URL belongs to a Branch domain so that + * the Linking API can skip it and avoid duplicate processing. + */ +export function isBranchDomainUrl(url: string): boolean { + try { + const hostname = new URL(url).hostname; + return BRANCH_DOMAIN_HOSTS.includes(hostname); + } catch { + return false; + } +} + /** * 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 @@ -20,28 +39,28 @@ export function rewriteBranchUri( params: BranchParams | undefined, ): string | undefined { try { - if (!uri || !params?.['+clicked_branch_link']) return uri; + if (!uri || !params?.['+clicked_branch_link']) return undefined; const rawPath = params.$deeplink_path; - if (typeof rawPath !== 'string') return uri; + if (typeof rawPath !== 'string') return undefined; 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; + return undefined; } } export class DeeplinkManager { - // singleton instance private static _instance: DeeplinkManager | null = null; public pendingDeeplink: string | null; + public cachedBranchParams: BranchParams | undefined; constructor() { this.pendingDeeplink = null; + this.cachedBranchParams = undefined; } static getInstance(): DeeplinkManager { @@ -83,7 +102,13 @@ export class DeeplinkManager { } static start() { - DeeplinkManager.getInstance(); + const instance = DeeplinkManager.getInstance(); + + const cacheBranchParams = (params: Record | undefined) => { + if (params && typeof params === 'object' && Object.keys(params).length) { + instance.cachedBranchParams = params as BranchParams; + } + }; const getBranchDeeplink = async (uri?: string) => { if (uri) { @@ -93,20 +118,14 @@ export class DeeplinkManager { try { const latestParams = await branch.getLatestReferringParams(); + cacheBranchParams(latestParams as Record | undefined); - // Cold start: params may contain a resolved Branch link with $deeplink_path. const rewritten = rewriteBranchUri( latestParams?.['~referring_link'] as string | undefined, latestParams as Record | undefined, ); if (rewritten) { handleDeeplink({ uri: rewritten }); - return; - } - - const deeplink = latestParams?.['+non_branch_link'] as string; - if (deeplink) { - handleDeeplink({ uri: deeplink }); } } catch (error) { Logger.error(error as Error, 'Error getting Branch deeplink'); @@ -135,12 +154,21 @@ export class DeeplinkManager { if (!url) { return; } + if (isBranchDomainUrl(url)) { + Logger.log( + `handleDeeplink:: skipping Branch domain URL from Linking: ${url}`, + ); + return; + } Logger.log(`handleDeeplink:: got initial URL ${url}`); handleDeeplink({ uri: url }); }); Linking.addEventListener('url', (params) => { const { url } = params; + if (isBranchDomainUrl(url)) { + return; + } handleDeeplink({ uri: url }); }); @@ -155,6 +183,7 @@ export class DeeplinkManager { const branchError = new Error(error); Logger.error(branchError, 'Error subscribing to branch.'); } + cacheBranchParams(opts.params as Record | undefined); const rewritten = rewriteBranchUri( opts.uri, opts.params as Record | undefined, diff --git a/app/core/DeeplinkManager/handlers/legacy/__tests__/handleUniversalLink.test.ts b/app/core/DeeplinkManager/handlers/legacy/__tests__/handleUniversalLink.test.ts index 58503ef59eef..3beae91e404d 100644 --- a/app/core/DeeplinkManager/handlers/legacy/__tests__/handleUniversalLink.test.ts +++ b/app/core/DeeplinkManager/handlers/legacy/__tests__/handleUniversalLink.test.ts @@ -78,9 +78,6 @@ jest.mock('../../../util/deeplinks/deepLinkAnalytics', () => ({ ), mapSupportedActionToRoute: jest.fn(() => 'test-route'), })); -jest.mock('react-native-branch', () => ({ - getLatestReferringParams: jest.fn(), -})); const mockSubtle = QuickCrypto.webcrypto.subtle as jest.Mocked< typeof QuickCrypto.webcrypto.subtle @@ -101,6 +98,7 @@ describe('handleUniversalLink', () => { const instance = { parse: mockParse, navigation: mockNavigation, + cachedBranchParams: undefined, } as unknown as DeeplinkManager; const handled = jest.fn(); @@ -2352,13 +2350,10 @@ describe('handleUniversalLink', () => { }); describe('Branch.io params integration', () => { - const branch = jest.requireMock('react-native-branch') as { - getLatestReferringParams: jest.Mock; - }; - beforeEach(() => { jest.clearAllMocks(); - branch.getLatestReferringParams.mockClear(); + (instance as { cachedBranchParams: unknown }).cachedBranchParams = + undefined; }); it('includes branchParams in analytics context for whitelisted actions', async () => { @@ -2366,7 +2361,8 @@ describe('handleUniversalLink', () => { '+clicked_branch_link': true, '+is_first_session': false, }; - branch.getLatestReferringParams.mockResolvedValue(mockBranchParams); + (instance as { cachedBranchParams: unknown }).cachedBranchParams = + mockBranchParams; url = `https://${AppConstants.MM_UNIVERSAL_LINK_HOST}/${ACTIONS.WC}?uri=wc:test`; urlObj = { @@ -2396,7 +2392,8 @@ describe('handleUniversalLink', () => { '+clicked_branch_link': true, '+is_first_session': true, }; - branch.getLatestReferringParams.mockResolvedValue(mockBranchParams); + (instance as { cachedBranchParams: unknown }).cachedBranchParams = + mockBranchParams; mockHandleDeepLinkModalDisplay.mockImplementation( async (callbackParams) => { @@ -2429,94 +2426,7 @@ describe('handleUniversalLink', () => { ); }); - it('includes undefined branchParams in analytics context when Branch.io returns null', async () => { - branch.getLatestReferringParams.mockResolvedValue(null); - - url = `https://${AppConstants.MM_UNIVERSAL_LINK_HOST}/${ACTIONS.WC}?uri=wc:test`; - urlObj = { - hostname: AppConstants.MM_UNIVERSAL_LINK_HOST, - pathname: `/${ACTIONS.WC}`, - href: url, - } as ReturnType['urlObj']; - - await handleUniversalLink({ - instance, - handled, - urlObj, - browserCallBack: mockBrowserCallBack, - url, - source: 'test-source', - }); - - expect(mockCreateEventBuilder).toHaveBeenCalledWith( - expect.objectContaining({ - branchParams: undefined, - }), - ); - }); - - it('includes undefined branchParams in analytics context when Branch.io returns empty object', async () => { - branch.getLatestReferringParams.mockResolvedValue({}); - - url = `https://${AppConstants.MM_UNIVERSAL_LINK_HOST}/${ACTIONS.WC}?uri=wc:test`; - urlObj = { - hostname: AppConstants.MM_UNIVERSAL_LINK_HOST, - pathname: `/${ACTIONS.WC}`, - href: url, - } as ReturnType['urlObj']; - - await handleUniversalLink({ - instance, - handled, - urlObj, - browserCallBack: mockBrowserCallBack, - url, - source: 'test-source', - }); - - expect(mockCreateEventBuilder).toHaveBeenCalledWith( - expect.objectContaining({ - branchParams: undefined, - }), - ); - }); - - it('includes undefined branchParams in analytics context when Branch.io fetch fails', async () => { - branch.getLatestReferringParams.mockRejectedValue( - new Error('Branch.io error'), - ); - - url = `https://${AppConstants.MM_UNIVERSAL_LINK_HOST}/${ACTIONS.WC}?uri=wc:test`; - urlObj = { - hostname: AppConstants.MM_UNIVERSAL_LINK_HOST, - pathname: `/${ACTIONS.WC}`, - href: url, - } as ReturnType['urlObj']; - - await handleUniversalLink({ - instance, - handled, - urlObj, - browserCallBack: mockBrowserCallBack, - url, - source: 'test-source', - }); - - expect(mockCreateEventBuilder).toHaveBeenCalledWith( - expect.objectContaining({ - branchParams: undefined, - }), - ); - }); - - it('includes undefined branchParams in analytics context when Branch.io fetch times out', async () => { - branch.getLatestReferringParams.mockImplementation( - () => - new Promise((resolve) => { - setTimeout(() => resolve({}), 1000); - }), - ); - + it('includes undefined branchParams in analytics context when no cached params', async () => { url = `https://${AppConstants.MM_UNIVERSAL_LINK_HOST}/${ACTIONS.WC}?uri=wc:test`; urlObj = { hostname: AppConstants.MM_UNIVERSAL_LINK_HOST, @@ -2533,7 +2443,6 @@ describe('handleUniversalLink', () => { source: 'test-source', }); - // Should still proceed with undefined branchParams expect(mockCreateEventBuilder).toHaveBeenCalledWith( expect.objectContaining({ branchParams: undefined, diff --git a/app/core/DeeplinkManager/handlers/legacy/handleUniversalLink.ts b/app/core/DeeplinkManager/handlers/legacy/handleUniversalLink.ts index f1f167b3ffd7..9cbdda32200b 100644 --- a/app/core/DeeplinkManager/handlers/legacy/handleUniversalLink.ts +++ b/app/core/DeeplinkManager/handlers/legacy/handleUniversalLink.ts @@ -52,8 +52,6 @@ import { isSupportedAction } from '../../types/deepLink.types'; import { selectDeepLinkModalDisabled } from '../../../../selectors/settings'; import ReduxService from '../../../redux'; import { analytics } from '../../../../util/analytics/analytics'; -import branch from 'react-native-branch'; -import Logger from '../../../../util/Logger'; const { MM_UNIVERSAL_LINK_HOST, @@ -286,40 +284,7 @@ async function handleUniversalLink({ // Extract URL params once (will be used for analytics) const { params } = extractURLParams(url); - /** - * Branch.io parameters for analytics context. - * Fetched once and reused across all analytics contexts to avoid duplicate API calls. - * May be undefined if fetch fails, times out, or returns empty/null data. - * Used by detectAppInstallation to determine app installation status. - */ - let branchParams: BranchParams | undefined; - try { - // Add timeout to prevent blocking deep link processing - const rawParams = await Promise.race([ - branch.getLatestReferringParams(), - new Promise((_, reject) => - setTimeout( - () => reject(new Error('Branch.io params fetch timeout')), - 500, - ), - ), - ]); - - // Validate before casting - handle null/empty edge cases - if ( - rawParams && - typeof rawParams === 'object' && - Object.keys(rawParams).length > 0 - ) { - branchParams = rawParams as BranchParams; - } - } catch (error) { - Logger.error( - error as Error, - 'DeepLinkManager: Error getting Branch.io params', - ); - // branchParams remains undefined - } + const branchParams: BranchParams | undefined = instance.cachedBranchParams; // Build analytics context - determine signature status // Check if signature parameter exists and has a value diff --git a/app/core/DeeplinkManager/util/deeplinks/index.ts b/app/core/DeeplinkManager/util/deeplinks/index.ts index 530628cb4e39..c46c38397a6b 100644 --- a/app/core/DeeplinkManager/util/deeplinks/index.ts +++ b/app/core/DeeplinkManager/util/deeplinks/index.ts @@ -8,16 +8,14 @@ const { } = AppConstants; const METAMASK_HOSTS = [ - ...new Set( - [ - MM_UNIVERSAL_LINK_HOST || 'link.metamask.io', - 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', - 'metamask.app.link', - 'metamask.test-app.link', - ].filter(Boolean), - ), + ...new Set([ + MM_UNIVERSAL_LINK_HOST, + MM_UNIVERSAL_LINK_HOST_ALTERNATE, + MM_IO_UNIVERSAL_LINK_HOST, + MM_IO_UNIVERSAL_LINK_TEST_HOST, + 'metamask.app.link', + 'metamask.test-app.link', + ]), ]; /** From aa488a5d9f7ff74d80657d68c3f53f03666d6194 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 18 Mar 2026 20:00:21 +0000 Subject: [PATCH 02/23] [skip ci] Bump version number to 4063 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 97165e049447..d9ef00e567a2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.71.0" - versionCode 3607 + versionCode 4063 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index c8f848cf5bda..877e460922f3 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3534,13 +3534,13 @@ app: VERSION_NAME: 7.71.0 - opts: is_expand: false - VERSION_NUMBER: 3911 + VERSION_NUMBER: 4063 - opts: is_expand: false FLASK_VERSION_NAME: 7.71.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3911 + FLASK_VERSION_NUMBER: 4063 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 6726013ffa4b..41b112463d3d 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3911; + CURRENT_PROJECT_VERSION = 4063; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3911; + CURRENT_PROJECT_VERSION = 4063; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3911; + CURRENT_PROJECT_VERSION = 4063; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3911; + CURRENT_PROJECT_VERSION = 4063; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3911; + CURRENT_PROJECT_VERSION = 4063; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3911; + CURRENT_PROJECT_VERSION = 4063; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 5f6730754211d537814760f899c98fff2eade8b8 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 18 Mar 2026 20:41:49 +0000 Subject: [PATCH 03/23] [skip ci] Bump version number to 4065 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d9ef00e567a2..ee84ade45e5c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.71.0" - versionCode 4063 + versionCode 4065 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 877e460922f3..fb968331045c 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3534,13 +3534,13 @@ app: VERSION_NAME: 7.71.0 - opts: is_expand: false - VERSION_NUMBER: 4063 + VERSION_NUMBER: 4065 - opts: is_expand: false FLASK_VERSION_NAME: 7.71.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 4063 + FLASK_VERSION_NUMBER: 4065 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 41b112463d3d..150adc50ea8d 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4063; + CURRENT_PROJECT_VERSION = 4065; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4063; + CURRENT_PROJECT_VERSION = 4065; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4063; + CURRENT_PROJECT_VERSION = 4065; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4063; + CURRENT_PROJECT_VERSION = 4065; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4063; + CURRENT_PROJECT_VERSION = 4065; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4063; + CURRENT_PROJECT_VERSION = 4065; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 80c527a3969a16a520df13aa7f7f3ee682823b7d Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Wed, 18 Mar 2026 21:04:02 +0000 Subject: [PATCH 04/23] feat: shortlink fix --- app/core/DeeplinkManager/DeeplinkManager.ts | 109 +++++++++++++++++++- 1 file changed, 104 insertions(+), 5 deletions(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index 516017419401..9e1f274722eb 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -14,6 +14,84 @@ const BRANCH_DOMAIN_HOSTS = [ AppConstants.MM_UNIVERSAL_LINK_HOST_ALTERNATE, ]; +/** + * Strips Branch Deepview query params from a URL to recover the original + * short link. The Deepview page appends __branch_*, sig, sig_params, and + * _referrer params that can confuse the Branch SDK's link resolution. + */ +export function stripBranchDeepviewParams(url: string): string { + try { + const parsed = new URL(url); + const paramsToStrip = [ + '__branch_flow_type', + '__branch_flow_id', + '__branch_mobile_deepview_type', + 'sig', + 'sig_params', + '_referrer', + ]; + for (const p of paramsToStrip) { + parsed.searchParams.delete(p); + } + return parsed.toString(); + } catch { + return url; + } +} + +/** + * When the Branch SDK fails to resolve a short link (returns +non_branch_link), + * fetch the short link URL directly and follow redirects. Branch's server will + * redirect to the destination URL (e.g. https://link.metamask.io/buy) which + * contains the routable path. If the redirect lands on a MetaMask host, use it. + * Otherwise, try to extract $deeplink_path from the HTML response body. + */ +export async function resolveBranchShortLink( + shortLinkUrl: string, +): Promise { + try { + const cleanUrl = stripBranchDeepviewParams(shortLinkUrl); + + const response = await fetch(cleanUrl, { + redirect: 'follow', + headers: { 'User-Agent': 'MetaMask-Mobile/1.0 (deep-link-resolver)' }, + }); + + const finalUrl = response.url; + + try { + const finalHostname = new URL(finalUrl).hostname; + if ( + finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_HOST || + finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_TEST_HOST + ) { + return finalUrl; + } + } catch { + // ignore URL parse errors on finalUrl + } + + const body = await response.text(); + + const deepLinkPathMatch = + body.match(/\$deeplink_path['":\s]+['"]([^'"]+)['"]/) ?? + body.match(/deeplink_path['":\s]+['"]([^'"]+)['"]/); + + if (deepLinkPathMatch?.[1]) { + const path = deepLinkPathMatch[1]; + return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${path.replace(/^\//, '')}`; + } + + return undefined; + } catch (error) { + Logger.error( + error as Error, + `Error resolving Branch short link: ${shortLinkUrl}`, + ); + return undefined; + } +} + /** * Branch domain URLs (metamask.app.link, metamask-alternate.app.link) are handled * by the Branch SDK. Returns true if the URL belongs to a Branch domain so that @@ -126,6 +204,16 @@ export class DeeplinkManager { ); if (rewritten) { handleDeeplink({ uri: rewritten }); + } else { + const nonBranchLink = latestParams?.['+non_branch_link'] as + | string + | undefined; + if (nonBranchLink && isBranchDomainUrl(nonBranchLink)) { + const resolved = await resolveBranchShortLink(nonBranchLink); + if (resolved) { + handleDeeplink({ uri: resolved }); + } + } } } catch (error) { Logger.error(error as Error, 'Error getting Branch deeplink'); @@ -155,12 +243,8 @@ export class DeeplinkManager { return; } if (isBranchDomainUrl(url)) { - Logger.log( - `handleDeeplink:: skipping Branch domain URL from Linking: ${url}`, - ); return; } - Logger.log(`handleDeeplink:: got initial URL ${url}`); handleDeeplink({ uri: url }); }); @@ -188,7 +272,22 @@ export class DeeplinkManager { opts.uri, opts.params as Record | undefined, ); - getBranchDeeplink(rewritten ?? opts.uri); + + if (rewritten) { + getBranchDeeplink(rewritten); + } else if (opts.uri && !isBranchDomainUrl(opts.uri)) { + getBranchDeeplink(opts.uri); + } else if ( + opts.uri && + isBranchDomainUrl(opts.uri) && + opts.params?.['+non_branch_link'] + ) { + resolveBranchShortLink(opts.uri).then((resolved) => { + if (resolved) { + getBranchDeeplink(resolved); + } + }); + } }); } } From 8f78e4904b473ee37405e1ef71a071afc9e19407 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 18 Mar 2026 21:09:11 +0000 Subject: [PATCH 05/23] [skip ci] Bump version number to 4067 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ee84ade45e5c..ab2b5e89ed00 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.71.0" - versionCode 4065 + versionCode 4067 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index fb968331045c..f7f8e1bdb7f7 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3534,13 +3534,13 @@ app: VERSION_NAME: 7.71.0 - opts: is_expand: false - VERSION_NUMBER: 4065 + VERSION_NUMBER: 4067 - opts: is_expand: false FLASK_VERSION_NAME: 7.71.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 4065 + FLASK_VERSION_NUMBER: 4067 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 150adc50ea8d..85aa66e91d25 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4065; + CURRENT_PROJECT_VERSION = 4067; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4065; + CURRENT_PROJECT_VERSION = 4067; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4065; + CURRENT_PROJECT_VERSION = 4067; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4065; + CURRENT_PROJECT_VERSION = 4067; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4065; + CURRENT_PROJECT_VERSION = 4067; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4065; + CURRENT_PROJECT_VERSION = 4067; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 4f15ab3feeb018f01f190a5ab73ab133b8d1bb87 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 19 Mar 2026 14:57:06 +0000 Subject: [PATCH 06/23] [skip ci] Bump version number to 4088 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ab2b5e89ed00..c27a524e5490 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.71.0" - versionCode 4067 + versionCode 4088 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index f7f8e1bdb7f7..1948438535e6 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3534,13 +3534,13 @@ app: VERSION_NAME: 7.71.0 - opts: is_expand: false - VERSION_NUMBER: 4067 + VERSION_NUMBER: 4088 - opts: is_expand: false FLASK_VERSION_NAME: 7.71.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 4067 + FLASK_VERSION_NUMBER: 4088 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 85aa66e91d25..588e75899b48 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4067; + CURRENT_PROJECT_VERSION = 4088; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4067; + CURRENT_PROJECT_VERSION = 4088; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4067; + CURRENT_PROJECT_VERSION = 4088; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4067; + CURRENT_PROJECT_VERSION = 4088; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4067; + CURRENT_PROJECT_VERSION = 4088; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4067; + CURRENT_PROJECT_VERSION = 4088; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From b245f0da013d2a609e1273d59996133f08a6311f Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 19 Mar 2026 15:29:03 +0000 Subject: [PATCH 07/23] [skip ci] Bump version number to 4091 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index c27a524e5490..b1fb97847b04 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.71.0" - versionCode 4088 + versionCode 4091 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 1948438535e6..5dac7f5a4774 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3534,13 +3534,13 @@ app: VERSION_NAME: 7.71.0 - opts: is_expand: false - VERSION_NUMBER: 4088 + VERSION_NUMBER: 4091 - opts: is_expand: false FLASK_VERSION_NAME: 7.71.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 4088 + FLASK_VERSION_NUMBER: 4091 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 588e75899b48..746a5dc91a1f 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4088; + CURRENT_PROJECT_VERSION = 4091; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4088; + CURRENT_PROJECT_VERSION = 4091; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4088; + CURRENT_PROJECT_VERSION = 4091; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4088; + CURRENT_PROJECT_VERSION = 4091; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4088; + CURRENT_PROJECT_VERSION = 4091; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4088; + CURRENT_PROJECT_VERSION = 4091; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 7ac15c965e6349dbf7a745e1398a3fc11fd510dc Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Thu, 19 Mar 2026 16:13:22 +0000 Subject: [PATCH 08/23] feat: addressing bugbot concerns --- app/core/DeeplinkManager/DeeplinkManager.test.ts | 8 ++++---- app/core/DeeplinkManager/DeeplinkManager.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.test.ts b/app/core/DeeplinkManager/DeeplinkManager.test.ts index a66c63eae44e..e05d4646333e 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.test.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.test.ts @@ -399,7 +399,7 @@ describe('DeeplinkManager.start Branch deeplink handling', () => { }); }); - it('passes URI through unchanged when +clicked_branch_link is false', async () => { + it('does not pass Branch domain URI through when +clicked_branch_link is false and no +non_branch_link', async () => { DeeplinkManager.start(); const callback = (branch.subscribe as jest.Mock).mock.calls[0][0]; const mockUri = 'https://metamask.app.link/swap?amount=100'; @@ -410,10 +410,10 @@ describe('DeeplinkManager.start Branch deeplink handling', () => { }); await new Promise((resolve) => setImmediate(resolve)); - expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockUri }); + expect(handleDeeplink).not.toHaveBeenCalled(); }); - it('passes URI through unchanged when $deeplink_path is missing', async () => { + it('does not pass Branch domain URI through when $deeplink_path is missing and no +non_branch_link', async () => { DeeplinkManager.start(); const callback = (branch.subscribe as jest.Mock).mock.calls[0][0]; const mockUri = 'https://metamask.app.link/swap?amount=100'; @@ -424,7 +424,7 @@ describe('DeeplinkManager.start Branch deeplink handling', () => { }); await new Promise((resolve) => setImmediate(resolve)); - expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockUri }); + expect(handleDeeplink).not.toHaveBeenCalled(); }); it('strips leading slash from $deeplink_path when rewriting', async () => { diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index 9e1f274722eb..c5a456a35e0b 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -52,11 +52,17 @@ export async function resolveBranchShortLink( try { const cleanUrl = stripBranchDeepviewParams(shortLinkUrl); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 3000); + const response = await fetch(cleanUrl, { redirect: 'follow', headers: { 'User-Agent': 'MetaMask-Mobile/1.0 (deep-link-resolver)' }, + signal: controller.signal, }); + clearTimeout(timeout); + const finalUrl = response.url; try { @@ -185,6 +191,8 @@ export class DeeplinkManager { const cacheBranchParams = (params: Record | undefined) => { if (params && typeof params === 'object' && Object.keys(params).length) { instance.cachedBranchParams = params as BranchParams; + } else { + instance.cachedBranchParams = undefined; } }; From a6d572dc31cff8d581e902de57f965cc609c6193 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 19 Mar 2026 16:29:56 +0000 Subject: [PATCH 09/23] [skip ci] Bump version number to 4092 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index b1fb97847b04..8f45d8e38058 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.71.0" - versionCode 4091 + versionCode 4092 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 5dac7f5a4774..dd60654e03bc 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3534,13 +3534,13 @@ app: VERSION_NAME: 7.71.0 - opts: is_expand: false - VERSION_NUMBER: 4091 + VERSION_NUMBER: 4092 - opts: is_expand: false FLASK_VERSION_NAME: 7.71.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 4091 + FLASK_VERSION_NUMBER: 4092 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 746a5dc91a1f..a7f1dae44e1f 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4091; + CURRENT_PROJECT_VERSION = 4092; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4091; + CURRENT_PROJECT_VERSION = 4092; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4091; + CURRENT_PROJECT_VERSION = 4092; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4091; + CURRENT_PROJECT_VERSION = 4092; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4091; + CURRENT_PROJECT_VERSION = 4092; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4091; + CURRENT_PROJECT_VERSION = 4092; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 2f4c80d3f8d8b8c7ca7e1ecc9f5fcf4d42ca1437 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Thu, 19 Mar 2026 17:35:49 +0000 Subject: [PATCH 10/23] feat: more test coverage --- .../DeeplinkManager/DeeplinkManager.test.ts | 336 ++++++++++++++++++ app/core/DeeplinkManager/DeeplinkManager.ts | 3 +- 2 files changed, 338 insertions(+), 1 deletion(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.test.ts b/app/core/DeeplinkManager/DeeplinkManager.test.ts index e05d4646333e..25f224bec2f7 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.test.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.test.ts @@ -6,6 +6,8 @@ import SharedDeeplinkManager, { DeeplinkManager, rewriteBranchUri, isBranchDomainUrl, + stripBranchDeepviewParams, + resolveBranchShortLink, } from './DeeplinkManager'; import type { BranchParams } from './types/deepLinkAnalytics.types'; import { handleDeeplink } from './handlers/legacy/handleDeeplink'; @@ -16,6 +18,7 @@ import { store } from '../../store'; import { RootState } from '../../reducers'; import branch from 'react-native-branch'; import AppConstants from '../AppConstants'; +import Logger from '../../util/Logger'; jest.mock('./handlers/legacy/handleApproveUrl'); jest.mock('./handlers/legacy/handleEthereumUrl'); @@ -30,6 +33,13 @@ jest.mock('./handlers/legacy/handleRewardsUrl'); jest.mock('./handlers/legacy/handleDeeplink'); jest.mock('./handlers/legacy/handleFastOnboarding'); jest.mock('../../util/notifications/services/FCMService'); +jest.mock('../../util/Logger', () => ({ + __esModule: true, + default: { + error: jest.fn(), + log: jest.fn(), + }, +})); jest.mock('../../store', () => ({ store: { getState: jest.fn(), @@ -333,6 +343,7 @@ describe('rewriteBranchUri', () => { describe('DeeplinkManager.start Branch deeplink handling', () => { beforeEach(() => { jest.clearAllMocks(); + (branch.getLatestReferringParams as jest.Mock).mockResolvedValue({}); }); it('calls getLatestReferringParams immediately for cold start deeplink check', async () => { @@ -533,3 +544,328 @@ describe('DeeplinkManager.start Linking API filters Branch domain URLs', () => { expect(handleDeeplink).toHaveBeenCalledWith({ uri: 'metamask://buy' }); }); }); + +describe('stripBranchDeepviewParams', () => { + it('removes Branch Deepview query params from URL', () => { + const url = + 'https://metamask-alternate.app.link/1WkF6GmE40b?__branch_flow_type=viewapp&__branch_flow_id=123&__branch_mobile_deepview_type=1&sig=abc&sig_params=foo&_referrer=twitter&utm_source=twitter'; + + const result = stripBranchDeepviewParams(url); + + expect(result).toBe( + 'https://metamask-alternate.app.link/1WkF6GmE40b?utm_source=twitter', + ); + }); + + it('returns URL unchanged when no Deepview params are present', () => { + const url = 'https://metamask-alternate.app.link/abc?utm_source=slack'; + + const result = stripBranchDeepviewParams(url); + + expect(result).toBe(url); + }); + + it('returns original string for invalid URLs', () => { + const result = stripBranchDeepviewParams('not-a-url'); + + expect(result).toBe('not-a-url'); + }); +}); + +describe('resolveBranchShortLink', () => { + const originalFetch = global.fetch; + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('returns final URL when redirect lands on link.metamask.io', async () => { + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://link.metamask.io/buy', + text: jest.fn().mockResolvedValue(''), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBe('https://link.metamask.io/buy'); + }); + + it('returns final URL when redirect lands on link-test.metamask.io', async () => { + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://link-test.metamask.io/swap', + text: jest.fn().mockResolvedValue(''), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBe('https://link-test.metamask.io/swap'); + }); + + it('extracts $deeplink_path from HTML body when redirect does not land on MetaMask host', async () => { + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/abc123', + text: jest + .fn() + .mockResolvedValue( + '', + ), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBe('https://link.metamask.io/swap'); + }); + + it('extracts deeplink_path without $ prefix from HTML body', async () => { + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/abc123', + text: jest + .fn() + .mockResolvedValue( + '', + ), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBe('https://link.metamask.io/buy'); + }); + + it('strips leading slash from extracted deeplink_path', async () => { + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/abc123', + text: jest + .fn() + .mockResolvedValue(''), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBe('https://link.metamask.io/perps'); + }); + + it('returns undefined when no deeplink_path found in response', async () => { + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/abc123', + text: jest.fn().mockResolvedValue('No data'), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBeUndefined(); + }); + + it('returns undefined and logs error when fetch throws', async () => { + global.fetch = jest.fn().mockRejectedValue(new Error('Network error')); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBeUndefined(); + }); + + it('strips Deepview params before fetching', async () => { + const mockFetch = jest.fn().mockResolvedValue({ + url: 'https://link.metamask.io/buy', + text: jest.fn().mockResolvedValue(''), + }); + global.fetch = mockFetch; + + await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123?__branch_flow_type=viewapp&_referrer=twitter', + ); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://metamask-alternate.app.link/abc123', + expect.objectContaining({ redirect: 'follow' }), + ); + }); + + it('handles invalid finalUrl gracefully', async () => { + global.fetch = jest.fn().mockResolvedValue({ + url: 'not-a-valid-url', + text: jest.fn().mockResolvedValue('"$deeplink_path": "swap"'), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBe('https://link.metamask.io/swap'); + }); +}); + +describe('DeeplinkManager.start Branch error and +non_branch_link handling', () => { + beforeEach(() => { + jest.clearAllMocks(); + (branch.getLatestReferringParams as jest.Mock).mockResolvedValue({}); + const { Linking } = jest.requireMock('react-native'); + (Linking.getInitialURL as jest.Mock).mockResolvedValue(null); + }); + + it('logs error when branch.subscribe receives an error', async () => { + const mockedLogger = jest.mocked(Logger); + DeeplinkManager.start(); + const callback = (branch.subscribe as jest.Mock).mock.calls[0][0]; + + callback({ error: 'Branch init failed', uri: undefined, params: {} }); + + expect(mockedLogger.error).toHaveBeenCalledWith( + expect.any(Error), + 'Error subscribing to branch.', + ); + }); + + it('resolves +non_branch_link on Branch domain via resolveBranchShortLink in subscribe', async () => { + const originalFetch = global.fetch; + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://link.metamask.io/buy', + text: jest.fn().mockResolvedValue(''), + }); + + DeeplinkManager.start(); + const callback = (branch.subscribe as jest.Mock).mock.calls[0][0]; + + callback({ + uri: 'https://metamask-alternate.app.link/1WkF6GmE40b', + params: { + '+clicked_branch_link': false, + '+non_branch_link': 'https://metamask-alternate.app.link/1WkF6GmE40b', + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(handleDeeplink).toHaveBeenCalledWith({ + uri: 'https://link.metamask.io/buy', + }); + + global.fetch = originalFetch; + }); + + it('does not call handleDeeplink when resolveBranchShortLink returns undefined in subscribe', async () => { + const originalFetch = global.fetch; + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/1WkF6GmE40b', + text: jest.fn().mockResolvedValue('no data'), + }); + + DeeplinkManager.start(); + const callback = (branch.subscribe as jest.Mock).mock.calls[0][0]; + + callback({ + uri: 'https://metamask-alternate.app.link/1WkF6GmE40b', + params: { + '+clicked_branch_link': false, + '+non_branch_link': 'https://metamask-alternate.app.link/1WkF6GmE40b', + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(handleDeeplink).not.toHaveBeenCalled(); + + global.fetch = originalFetch; + }); + + it('resolves +non_branch_link on Branch domain via resolveBranchShortLink on cold start', async () => { + const originalFetch = global.fetch; + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://link.metamask.io/swap', + text: jest.fn().mockResolvedValue(''), + }); + + (branch.getLatestReferringParams as jest.Mock).mockResolvedValue({ + '+clicked_branch_link': false, + '+non_branch_link': 'https://metamask-alternate.app.link/1WkF6GmE40b', + }); + + DeeplinkManager.start(); + + await waitFor(() => { + expect(handleDeeplink).toHaveBeenCalledWith({ + uri: 'https://link.metamask.io/swap', + }); + }); + + global.fetch = originalFetch; + }); + + it('caches Branch params and clears them when empty', async () => { + DeeplinkManager.start(); + const instance = DeeplinkManager.getInstance(); + const callback = (branch.subscribe as jest.Mock).mock.calls[0][0]; + + await new Promise((resolve) => setImmediate(resolve)); + + callback({ + uri: 'https://link.metamask.io/buy', + params: { + '+clicked_branch_link': true, + $deeplink_path: 'buy', + '~campaign': 'test', + }, + }); + + expect(instance.cachedBranchParams).toBeDefined(); + expect(instance.cachedBranchParams?.['~campaign']).toBe('test'); + + callback({ + uri: 'https://link.metamask.io/home', + params: undefined, + }); + + expect(instance.cachedBranchParams).toBeUndefined(); + }); + + it('logs error when getLatestReferringParams throws on cold start', async () => { + const mockedLogger = jest.mocked(Logger); + (branch.getLatestReferringParams as jest.Mock).mockRejectedValue( + new Error('Branch SDK error'), + ); + + DeeplinkManager.start(); + + await waitFor(() => { + expect(mockedLogger.error).toHaveBeenCalledWith( + expect.any(Error), + 'Error getting Branch deeplink', + ); + }); + }); +}); + +describe('rewriteBranchUri error handling', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns undefined and logs error for malformed URI', () => { + const mockedLogger = jest.mocked(Logger); + + const result = rewriteBranchUri(':::invalid-url', { + '+clicked_branch_link': true, + $deeplink_path: 'swap', + } as BranchParams); + + expect(result).toBeUndefined(); + expect(mockedLogger.error).toHaveBeenCalledTimes(1); + const [errorArg, msgArg] = mockedLogger.error.mock.calls[0]; + expect(errorArg).toBeDefined(); + expect(errorArg.message).toBeDefined(); + expect(msgArg).toContain('Error rewriting Branch URI'); + }); +}); diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index c5a456a35e0b..a00ccacda196 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -290,7 +290,8 @@ export class DeeplinkManager { isBranchDomainUrl(opts.uri) && opts.params?.['+non_branch_link'] ) { - resolveBranchShortLink(opts.uri).then((resolved) => { + const nonBranchLink = opts.params['+non_branch_link'] as string; + resolveBranchShortLink(nonBranchLink).then((resolved) => { if (resolved) { getBranchDeeplink(resolved); } From bb6bc74a069bea7bd254ce417c8305eb98923bf4 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Thu, 19 Mar 2026 18:08:30 +0000 Subject: [PATCH 11/23] feat: fix deeplinking issue --- .../DeeplinkManager/DeeplinkManager.test.ts | 67 ++++++++++++++++++- app/core/DeeplinkManager/DeeplinkManager.ts | 46 +++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.test.ts b/app/core/DeeplinkManager/DeeplinkManager.test.ts index 25f224bec2f7..285871eeae35 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.test.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.test.ts @@ -706,6 +706,71 @@ describe('resolveBranchShortLink', () => { expect(result).toBe('https://link.metamask.io/swap'); }); + + it('extracts path from Deepview launch button with null scheme prefix', async () => { + const deepviewHtml = ` + Launch MetaMask + + `; + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/abc123', + text: jest.fn().mockResolvedValue(deepviewHtml), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBe('https://link.metamask.io/trending'); + }); + + it('extracts path from Deepview launch button with metamask:// scheme', async () => { + const deepviewHtml = ` + Launch MetaMask + `; + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/abc123', + text: jest.fn().mockResolvedValue(deepviewHtml), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBe('https://link.metamask.io/buy'); + }); + + it('extracts path from Deepview launch button with full MetaMask URL', async () => { + const deepviewHtml = ` + Launch MetaMask + `; + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/abc123', + text: jest.fn().mockResolvedValue(deepviewHtml), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBe('https://link.metamask.io/swap'); + }); + + it('falls back to window.top.location when no action button found', async () => { + const deepviewHtml = ` + + `; + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/abc123', + text: jest.fn().mockResolvedValue(deepviewHtml), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc123', + ); + + expect(result).toBe('https://link.metamask.io/perps'); + }); }); describe('DeeplinkManager.start Branch error and +non_branch_link handling', () => { @@ -842,7 +907,7 @@ describe('DeeplinkManager.start Branch error and +non_branch_link handling', () await waitFor(() => { expect(mockedLogger.error).toHaveBeenCalledWith( expect.any(Error), - 'Error getting Branch deeplink', + expect.stringContaining('Error getting Branch deeplink'), ); }); }); diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index a00ccacda196..1c4322b61d82 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -39,6 +39,48 @@ export function stripBranchDeepviewParams(url: string): string { } } +/** + * Branch Deepview pages embed the app launch URL in two places: + * 1. `` + * 2. `window.top.location = validateProtocol("{scheme}{path}?...")` + * + * The scheme may be "metamask://", "https://link.metamask.io/", or literally + * "null" (when the Branch link has no URI scheme configured). This function + * extracts the deeplink path from these patterns. + */ +function extractDeepviewPath(html: string): string | undefined { + const launchHref = + html.match(/]*class="action"[^>]*href="([^"?]+)/)?.[1] ?? + html.match(/window\.top\.location\s*=\s*validateProtocol\("([^"?]+)/)?.[1]; + + if (!launchHref) return undefined; + + if (launchHref.startsWith('null') && launchHref.length > 4) { + return launchHref.substring(4); + } + + if (launchHref.startsWith('metamask://')) { + return launchHref.replace('metamask://', ''); + } + + try { + const parsed = new URL(launchHref); + if ( + parsed.hostname === AppConstants.MM_IO_UNIVERSAL_LINK_HOST || + parsed.hostname === AppConstants.MM_IO_UNIVERSAL_LINK_TEST_HOST + ) { + return parsed.pathname.replace(/^\//, ''); + } + } catch { + // not a full URL — treat it as a raw path + if (/^[a-zA-Z0-9]/.test(launchHref)) { + return launchHref; + } + } + + return undefined; +} + /** * When the Branch SDK fails to resolve a short link (returns +non_branch_link), * fetch the short link URL directly and follow redirects. Branch's server will @@ -88,6 +130,10 @@ export async function resolveBranchShortLink( return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${path.replace(/^\//, '')}`; } + const deepviewPath = extractDeepviewPath(body); + if (deepviewPath) { + return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${deepviewPath.replace(/^\//, '')}`; + } return undefined; } catch (error) { Logger.error( From 3de37a9335a9131f2ba2ba8d8c63b42bdb7fa53b Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 19 Mar 2026 18:30:36 +0000 Subject: [PATCH 12/23] [skip ci] Bump version number to 4097 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 8f45d8e38058..d80c78abc0cb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.71.0" - versionCode 4092 + versionCode 4097 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index dd60654e03bc..9614f7187d84 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3534,13 +3534,13 @@ app: VERSION_NAME: 7.71.0 - opts: is_expand: false - VERSION_NUMBER: 4092 + VERSION_NUMBER: 4097 - opts: is_expand: false FLASK_VERSION_NAME: 7.71.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 4092 + FLASK_VERSION_NUMBER: 4097 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index a7f1dae44e1f..e68e28910cf5 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4092; + CURRENT_PROJECT_VERSION = 4097; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4092; + CURRENT_PROJECT_VERSION = 4097; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4092; + CURRENT_PROJECT_VERSION = 4097; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4092; + CURRENT_PROJECT_VERSION = 4097; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4092; + CURRENT_PROJECT_VERSION = 4097; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4092; + CURRENT_PROJECT_VERSION = 4097; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 456b59d2eb499cc9a7fe41f3616d7926c8aabcd6 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 19 Mar 2026 18:42:00 +0000 Subject: [PATCH 13/23] [skip ci] Bump version number to 4099 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d80c78abc0cb..9c53e81a0d4c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.71.0" - versionCode 4097 + versionCode 4099 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 9614f7187d84..25ceadddc959 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3534,13 +3534,13 @@ app: VERSION_NAME: 7.71.0 - opts: is_expand: false - VERSION_NUMBER: 4097 + VERSION_NUMBER: 4099 - opts: is_expand: false FLASK_VERSION_NAME: 7.71.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 4097 + FLASK_VERSION_NUMBER: 4099 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index e68e28910cf5..5caa454d5a26 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4097; + CURRENT_PROJECT_VERSION = 4099; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4097; + CURRENT_PROJECT_VERSION = 4099; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4097; + CURRENT_PROJECT_VERSION = 4099; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4097; + CURRENT_PROJECT_VERSION = 4099; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4097; + CURRENT_PROJECT_VERSION = 4099; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4097; + CURRENT_PROJECT_VERSION = 4099; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 1184a3d630adfc76c34a614a930008d660d9fd9f Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Thu, 19 Mar 2026 21:38:02 +0000 Subject: [PATCH 14/23] feat: small changes --- app/core/DeeplinkManager/DeeplinkManager.test.ts | 4 ++-- app/core/DeeplinkManager/DeeplinkManager.ts | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.test.ts b/app/core/DeeplinkManager/DeeplinkManager.test.ts index 285871eeae35..2dcf1f60324f 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.test.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.test.ts @@ -546,14 +546,14 @@ describe('DeeplinkManager.start Linking API filters Branch domain URLs', () => { }); describe('stripBranchDeepviewParams', () => { - it('removes Branch Deepview query params from URL', () => { + it('removes Branch Deepview query params from URL but preserves sig and sig_params', () => { const url = 'https://metamask-alternate.app.link/1WkF6GmE40b?__branch_flow_type=viewapp&__branch_flow_id=123&__branch_mobile_deepview_type=1&sig=abc&sig_params=foo&_referrer=twitter&utm_source=twitter'; const result = stripBranchDeepviewParams(url); expect(result).toBe( - 'https://metamask-alternate.app.link/1WkF6GmE40b?utm_source=twitter', + 'https://metamask-alternate.app.link/1WkF6GmE40b?sig=abc&sig_params=foo&utm_source=twitter', ); }); diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index 1c4322b61d82..44eaa6c1f813 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -16,8 +16,8 @@ const BRANCH_DOMAIN_HOSTS = [ /** * Strips Branch Deepview query params from a URL to recover the original - * short link. The Deepview page appends __branch_*, sig, sig_params, and - * _referrer params that can confuse the Branch SDK's link resolution. + * short link. The Deepview page appends __branch_* and _referrer params + * that can confuse the Branch SDK's link resolution. */ export function stripBranchDeepviewParams(url: string): string { try { @@ -26,8 +26,6 @@ export function stripBranchDeepviewParams(url: string): string { '__branch_flow_type', '__branch_flow_id', '__branch_mobile_deepview_type', - 'sig', - 'sig_params', '_referrer', ]; for (const p of paramsToStrip) { From 044dc99f54d22a11d327b1fdaa4f88fbf7fe2143 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Thu, 19 Mar 2026 22:00:57 +0000 Subject: [PATCH 15/23] feat: bugbot --- app/core/DeeplinkManager/DeeplinkManager.ts | 96 +++++++++++++++++-- .../handlers/legacy/handleDeeplink.ts | 6 ++ 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index 44eaa6c1f813..40726ccf2a8d 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -89,8 +89,14 @@ function extractDeepviewPath(html: string): string | undefined { export async function resolveBranchShortLink( shortLinkUrl: string, ): Promise { + Logger.log( + `=== DEBUG === [resolveBranchShortLink] called with: ${shortLinkUrl}`, + ); try { const cleanUrl = stripBranchDeepviewParams(shortLinkUrl); + Logger.log( + `=== DEBUG === [resolveBranchShortLink] fetching cleanUrl=${cleanUrl}`, + ); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3000); @@ -104,6 +110,9 @@ export async function resolveBranchShortLink( clearTimeout(timeout); const finalUrl = response.url; + Logger.log( + `=== DEBUG === [resolveBranchShortLink] fetch done, status=${response.status}, finalUrl=${finalUrl}`, + ); try { const finalHostname = new URL(finalUrl).hostname; @@ -111,13 +120,24 @@ export async function resolveBranchShortLink( finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_HOST || finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_TEST_HOST ) { + Logger.log( + `=== DEBUG === [resolveBranchShortLink] finalUrl is MetaMask host, returning: ${finalUrl}`, + ); return finalUrl; } + Logger.log( + `=== DEBUG === [resolveBranchShortLink] finalUrl host=${finalHostname} is NOT MetaMask, parsing body`, + ); } catch { - // ignore URL parse errors on finalUrl + Logger.log( + `=== DEBUG === [resolveBranchShortLink] finalUrl parse error, parsing body`, + ); } const body = await response.text(); + Logger.log( + `=== DEBUG === [resolveBranchShortLink] body length=${body.length}`, + ); const deepLinkPathMatch = body.match(/\$deeplink_path['":\s]+['"]([^'"]+)['"]/) ?? @@ -125,18 +145,29 @@ export async function resolveBranchShortLink( if (deepLinkPathMatch?.[1]) { const path = deepLinkPathMatch[1]; - return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${path.replace(/^\//, '')}`; + const result = `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${path.replace(/^\//, '')}`; + Logger.log( + `=== DEBUG === [resolveBranchShortLink] extracted $deeplink_path="${path}", returning: ${result}`, + ); + return result; } const deepviewPath = extractDeepviewPath(body); if (deepviewPath) { - return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${deepviewPath.replace(/^\//, '')}`; + const result = `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${deepviewPath.replace(/^\//, '')}`; + Logger.log( + `=== DEBUG === [resolveBranchShortLink] extracted Deepview path="${deepviewPath}", returning: ${result}`, + ); + return result; } + Logger.log( + `=== DEBUG === [resolveBranchShortLink] no deeplink path found, returning undefined`, + ); return undefined; } catch (error) { Logger.error( error as Error, - `Error resolving Branch short link: ${shortLinkUrl}`, + `=== DEBUG === [resolveBranchShortLink] Error resolving: ${shortLinkUrl}`, ); return undefined; } @@ -241,7 +272,11 @@ export class DeeplinkManager { }; const getBranchDeeplink = async (uri?: string) => { + Logger.log(`=== DEBUG === [getBranchDeeplink] called with uri=${uri}`); if (uri) { + Logger.log( + `=== DEBUG === [getBranchDeeplink] dispatching handleDeeplink: ${uri}`, + ); handleDeeplink({ uri }); return; } @@ -291,20 +326,34 @@ export class DeeplinkManager { }); Linking.getInitialURL().then((url) => { + Logger.log(`=== DEBUG === [Linking.getInitialURL] url=${url}`); if (!url) { return; } if (isBranchDomainUrl(url)) { + Logger.log( + `=== DEBUG === [Linking.getInitialURL] skipping Branch domain: ${url}`, + ); return; } + Logger.log( + `=== DEBUG === [Linking.getInitialURL] dispatching handleDeeplink: ${url}`, + ); handleDeeplink({ uri: url }); }); Linking.addEventListener('url', (params) => { const { url } = params; + Logger.log(`=== DEBUG === [Linking.addEventListener] url=${url}`); if (isBranchDomainUrl(url)) { + Logger.log( + `=== DEBUG === [Linking.addEventListener] skipping Branch domain: ${url}`, + ); return; } + Logger.log( + `=== DEBUG === [Linking.addEventListener] dispatching handleDeeplink: ${url}`, + ); handleDeeplink({ uri: url }); }); @@ -314,6 +363,9 @@ export class DeeplinkManager { getBranchDeeplink(); branch.subscribe((opts) => { + Logger.log( + `=== DEBUG === [branch.subscribe] fired — uri=${opts.uri}, error=${opts.error}, params=${JSON.stringify(opts.params)}`, + ); const { error } = opts; if (error) { const branchError = new Error(error); @@ -326,20 +378,44 @@ export class DeeplinkManager { ); if (rewritten) { + Logger.log( + `=== DEBUG === [branch.subscribe] rewrite succeeded: ${rewritten}`, + ); getBranchDeeplink(rewritten); } else if (opts.uri && !isBranchDomainUrl(opts.uri)) { + Logger.log( + `=== DEBUG === [branch.subscribe] non-Branch domain URI, passing through: ${opts.uri}`, + ); getBranchDeeplink(opts.uri); } else if ( opts.uri && isBranchDomainUrl(opts.uri) && - opts.params?.['+non_branch_link'] + opts.params?.['+non_branch_link'] && + isBranchDomainUrl(opts.params['+non_branch_link'] as string) ) { const nonBranchLink = opts.params['+non_branch_link'] as string; - resolveBranchShortLink(nonBranchLink).then((resolved) => { - if (resolved) { - getBranchDeeplink(resolved); - } - }); + Logger.log( + `=== DEBUG === [branch.subscribe] Branch domain + +non_branch_link, resolving: ${nonBranchLink}`, + ); + resolveBranchShortLink(nonBranchLink) + .then((resolved) => { + Logger.log( + `=== DEBUG === [branch.subscribe] resolveBranchShortLink returned: ${resolved}`, + ); + if (resolved) { + getBranchDeeplink(resolved); + } + }) + .catch((err) => { + Logger.error( + err as Error, + 'Error resolving Branch short link in subscribe', + ); + }); + } else { + Logger.log( + `=== DEBUG === [branch.subscribe] no path matched — uri=${opts.uri}, dropped`, + ); } }); } diff --git a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts index d632a6d3a4fc..c8ed9c7d6f33 100644 --- a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts +++ b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts @@ -5,6 +5,9 @@ import ReduxService from '../../../redux'; import SDKConnectV2 from '../../../SDKConnectV2'; export function handleDeeplink(opts: { uri?: string; source?: string }) { + Logger.log( + `=== DEBUG === [handleDeeplink] called with uri=${opts.uri}, source=${opts.source}`, + ); // This is the earliest JS entry point for deeplinks. We must handle SDKConnectV2 // links here immediately to establish the WebSocket connection as fast as possible, // without waiting for the app to be unlocked or fully onboarded. @@ -20,6 +23,9 @@ export function handleDeeplink(opts: { uri?: string; source?: string }) { const { uri, source } = opts; try { if (uri && typeof uri === 'string') { + Logger.log( + `=== DEBUG === [handleDeeplink] setting currentDeeplink: ${uri}`, + ); AppStateEventProcessor.setCurrentDeeplink(uri, source); dispatch(checkForDeeplink()); } From 92a0ab0b4badbef65be1258713851ee50f742b81 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Thu, 19 Mar 2026 22:02:45 +0000 Subject: [PATCH 16/23] feat: removed logs --- app/core/DeeplinkManager/DeeplinkManager.ts | 76 +------------------ .../handlers/legacy/handleDeeplink.ts | 6 -- 2 files changed, 4 insertions(+), 78 deletions(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index 40726ccf2a8d..566d4828b731 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -89,14 +89,8 @@ function extractDeepviewPath(html: string): string | undefined { export async function resolveBranchShortLink( shortLinkUrl: string, ): Promise { - Logger.log( - `=== DEBUG === [resolveBranchShortLink] called with: ${shortLinkUrl}`, - ); try { const cleanUrl = stripBranchDeepviewParams(shortLinkUrl); - Logger.log( - `=== DEBUG === [resolveBranchShortLink] fetching cleanUrl=${cleanUrl}`, - ); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3000); @@ -110,9 +104,6 @@ export async function resolveBranchShortLink( clearTimeout(timeout); const finalUrl = response.url; - Logger.log( - `=== DEBUG === [resolveBranchShortLink] fetch done, status=${response.status}, finalUrl=${finalUrl}`, - ); try { const finalHostname = new URL(finalUrl).hostname; @@ -120,24 +111,13 @@ export async function resolveBranchShortLink( finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_HOST || finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_TEST_HOST ) { - Logger.log( - `=== DEBUG === [resolveBranchShortLink] finalUrl is MetaMask host, returning: ${finalUrl}`, - ); return finalUrl; } - Logger.log( - `=== DEBUG === [resolveBranchShortLink] finalUrl host=${finalHostname} is NOT MetaMask, parsing body`, - ); } catch { - Logger.log( - `=== DEBUG === [resolveBranchShortLink] finalUrl parse error, parsing body`, - ); + // ignore URL parse errors on finalUrl } const body = await response.text(); - Logger.log( - `=== DEBUG === [resolveBranchShortLink] body length=${body.length}`, - ); const deepLinkPathMatch = body.match(/\$deeplink_path['":\s]+['"]([^'"]+)['"]/) ?? @@ -145,29 +125,18 @@ export async function resolveBranchShortLink( if (deepLinkPathMatch?.[1]) { const path = deepLinkPathMatch[1]; - const result = `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${path.replace(/^\//, '')}`; - Logger.log( - `=== DEBUG === [resolveBranchShortLink] extracted $deeplink_path="${path}", returning: ${result}`, - ); - return result; + return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${path.replace(/^\//, '')}`; } const deepviewPath = extractDeepviewPath(body); if (deepviewPath) { - const result = `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${deepviewPath.replace(/^\//, '')}`; - Logger.log( - `=== DEBUG === [resolveBranchShortLink] extracted Deepview path="${deepviewPath}", returning: ${result}`, - ); - return result; + return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${deepviewPath.replace(/^\//, '')}`; } - Logger.log( - `=== DEBUG === [resolveBranchShortLink] no deeplink path found, returning undefined`, - ); return undefined; } catch (error) { Logger.error( error as Error, - `=== DEBUG === [resolveBranchShortLink] Error resolving: ${shortLinkUrl}`, + `Error resolving Branch short link: ${shortLinkUrl}`, ); return undefined; } @@ -272,11 +241,7 @@ export class DeeplinkManager { }; const getBranchDeeplink = async (uri?: string) => { - Logger.log(`=== DEBUG === [getBranchDeeplink] called with uri=${uri}`); if (uri) { - Logger.log( - `=== DEBUG === [getBranchDeeplink] dispatching handleDeeplink: ${uri}`, - ); handleDeeplink({ uri }); return; } @@ -326,34 +291,20 @@ export class DeeplinkManager { }); Linking.getInitialURL().then((url) => { - Logger.log(`=== DEBUG === [Linking.getInitialURL] url=${url}`); if (!url) { return; } if (isBranchDomainUrl(url)) { - Logger.log( - `=== DEBUG === [Linking.getInitialURL] skipping Branch domain: ${url}`, - ); return; } - Logger.log( - `=== DEBUG === [Linking.getInitialURL] dispatching handleDeeplink: ${url}`, - ); handleDeeplink({ uri: url }); }); Linking.addEventListener('url', (params) => { const { url } = params; - Logger.log(`=== DEBUG === [Linking.addEventListener] url=${url}`); if (isBranchDomainUrl(url)) { - Logger.log( - `=== DEBUG === [Linking.addEventListener] skipping Branch domain: ${url}`, - ); return; } - Logger.log( - `=== DEBUG === [Linking.addEventListener] dispatching handleDeeplink: ${url}`, - ); handleDeeplink({ uri: url }); }); @@ -363,9 +314,6 @@ export class DeeplinkManager { getBranchDeeplink(); branch.subscribe((opts) => { - Logger.log( - `=== DEBUG === [branch.subscribe] fired — uri=${opts.uri}, error=${opts.error}, params=${JSON.stringify(opts.params)}`, - ); const { error } = opts; if (error) { const branchError = new Error(error); @@ -378,14 +326,8 @@ export class DeeplinkManager { ); if (rewritten) { - Logger.log( - `=== DEBUG === [branch.subscribe] rewrite succeeded: ${rewritten}`, - ); getBranchDeeplink(rewritten); } else if (opts.uri && !isBranchDomainUrl(opts.uri)) { - Logger.log( - `=== DEBUG === [branch.subscribe] non-Branch domain URI, passing through: ${opts.uri}`, - ); getBranchDeeplink(opts.uri); } else if ( opts.uri && @@ -394,14 +336,8 @@ export class DeeplinkManager { isBranchDomainUrl(opts.params['+non_branch_link'] as string) ) { const nonBranchLink = opts.params['+non_branch_link'] as string; - Logger.log( - `=== DEBUG === [branch.subscribe] Branch domain + +non_branch_link, resolving: ${nonBranchLink}`, - ); resolveBranchShortLink(nonBranchLink) .then((resolved) => { - Logger.log( - `=== DEBUG === [branch.subscribe] resolveBranchShortLink returned: ${resolved}`, - ); if (resolved) { getBranchDeeplink(resolved); } @@ -412,10 +348,6 @@ export class DeeplinkManager { 'Error resolving Branch short link in subscribe', ); }); - } else { - Logger.log( - `=== DEBUG === [branch.subscribe] no path matched — uri=${opts.uri}, dropped`, - ); } }); } diff --git a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts index c8ed9c7d6f33..d632a6d3a4fc 100644 --- a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts +++ b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts @@ -5,9 +5,6 @@ import ReduxService from '../../../redux'; import SDKConnectV2 from '../../../SDKConnectV2'; export function handleDeeplink(opts: { uri?: string; source?: string }) { - Logger.log( - `=== DEBUG === [handleDeeplink] called with uri=${opts.uri}, source=${opts.source}`, - ); // This is the earliest JS entry point for deeplinks. We must handle SDKConnectV2 // links here immediately to establish the WebSocket connection as fast as possible, // without waiting for the app to be unlocked or fully onboarded. @@ -23,9 +20,6 @@ export function handleDeeplink(opts: { uri?: string; source?: string }) { const { uri, source } = opts; try { if (uri && typeof uri === 'string') { - Logger.log( - `=== DEBUG === [handleDeeplink] setting currentDeeplink: ${uri}`, - ); AppStateEventProcessor.setCurrentDeeplink(uri, source); dispatch(checkForDeeplink()); } From 7d5bd3bd8ef701f4d304b2683d9a81a414f451db Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Thu, 19 Mar 2026 23:02:26 +0000 Subject: [PATCH 17/23] feat: fix x deeplink issue --- .../DeeplinkManager/DeeplinkManager.test.ts | 141 ++++++++++++++++++ app/core/DeeplinkManager/DeeplinkManager.ts | 76 +++++++++- 2 files changed, 212 insertions(+), 5 deletions(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.test.ts b/app/core/DeeplinkManager/DeeplinkManager.test.ts index 2dcf1f60324f..5c0dcdb3b7de 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.test.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.test.ts @@ -8,6 +8,7 @@ import SharedDeeplinkManager, { isBranchDomainUrl, stripBranchDeepviewParams, resolveBranchShortLink, + extractBranchUrlFromDeepview, } from './DeeplinkManager'; import type { BranchParams } from './types/deepLinkAnalytics.types'; import { handleDeeplink } from './handlers/legacy/handleDeeplink'; @@ -605,6 +606,67 @@ describe('resolveBranchShortLink', () => { expect(result).toBe('https://link-test.metamask.io/swap'); }); + it('extracts +url from link_click_id in Deepview HTML before falling back to $deeplink_path', async () => { + const b64 = + 'eyIrdXJsIjoiaHR0cHM6Ly9saW5rLm1ldGFtYXNrLmlvL2J1eSIsIiRkZWVwbGlua19wYXRoIjoib3BlbiJ9'; + const deepviewHtml = ` + + `; + + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/DudG79nFJ0b', + text: jest.fn().mockResolvedValue(deepviewHtml), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/DudG79nFJ0b?__branch_flow_type=viewapp&chainId=59144&address=0xABC&amount=25&_referrer=twitter', + ); + + expect(result).toBe( + 'https://link.metamask.io/buy?chainId=59144&address=0xABC&amount=25', + ); + }); + + it('merges query params from original URL onto +url from link_click_id', async () => { + const b64 = + 'eyIrdXJsIjoiaHR0cHM6Ly9saW5rLm1ldGFtYXNrLmlvL3RyZW5kaW5nIiwiJGRlZXBsaW5rX3BhdGgiOiJ0cmVuZGluZyJ9'; + const deepviewHtml = ` + + `; + + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/1WkF6GmE40b', + text: jest.fn().mockResolvedValue(deepviewHtml), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/1WkF6GmE40b?utm_source=twitter&utm_medium=social', + ); + + expect(result).toBe( + 'https://link.metamask.io/trending?utm_source=twitter&utm_medium=social', + ); + }); + + it('returns +url without merge when original URL has no extra params', async () => { + const b64 = + 'eyIrdXJsIjoiaHR0cHM6Ly9saW5rLm1ldGFtYXNrLmlvL2J1eSIsIiRkZWVwbGlua19wYXRoIjoib3BlbiJ9'; + const deepviewHtml = ` + + `; + + global.fetch = jest.fn().mockResolvedValue({ + url: 'https://metamask-alternate.app.link/abc', + text: jest.fn().mockResolvedValue(deepviewHtml), + }); + + const result = await resolveBranchShortLink( + 'https://metamask-alternate.app.link/abc', + ); + + expect(result).toBe('https://link.metamask.io/buy'); + }); + it('extracts $deeplink_path from HTML body when redirect does not land on MetaMask host', async () => { global.fetch = jest.fn().mockResolvedValue({ url: 'https://metamask-alternate.app.link/abc123', @@ -913,6 +975,85 @@ describe('DeeplinkManager.start Branch error and +non_branch_link handling', () }); }); +describe('extractBranchUrlFromDeepview', () => { + const buildDeepviewHtml = (linkClickId: string) => + ` + + `; + + it('extracts +url from valid link_click_id base64 JSON (trending link)', () => { + const b64 = + 'eyIrdXJsIjoiaHR0cHM6Ly9saW5rLm1ldGFtYXNrLmlvL3RyZW5kaW5nIiwiJGRlZXBsaW5rX3BhdGgiOiJ0cmVuZGluZyJ9'; + const html = buildDeepviewHtml(`link-12345-${b64}`); + + const result = extractBranchUrlFromDeepview(html); + + expect(result).toBe('https://link.metamask.io/trending'); + }); + + it('extracts +url from valid link_click_id base64 JSON (buy link)', () => { + const b64 = + 'eyIrdXJsIjoiaHR0cHM6Ly9saW5rLm1ldGFtYXNrLmlvL2J1eSIsIiRkZWVwbGlua19wYXRoIjoib3BlbiJ9'; + const html = buildDeepviewHtml(`link-9876543-${b64}`); + + const result = extractBranchUrlFromDeepview(html); + + expect(result).toBe('https://link.metamask.io/buy'); + }); + + it('returns undefined when al:ios:url meta tag is missing', () => { + const html = ''; + + const result = extractBranchUrlFromDeepview(html); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when link_click_id param is missing', () => { + const html = ` + + `; + + const result = extractBranchUrlFromDeepview(html); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when base64 payload is malformed', () => { + const html = buildDeepviewHtml('link-12345-!!!invalid-base64!!!'); + + const result = extractBranchUrlFromDeepview(html); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when JSON does not contain +url', () => { + const b64 = 'eyIkZGVlcGxpbmtfcGF0aCI6InN3YXAifQ'; + const html = buildDeepviewHtml(`link-12345-${b64}`); + + const result = extractBranchUrlFromDeepview(html); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when +url hostname is not a MetaMask host', () => { + const b64 = 'eyIrdXJsIjoiaHR0cHM6Ly9ldmlsLmNvbS9waGlzaCJ9'; + const html = buildDeepviewHtml(`link-12345-${b64}`); + + const result = extractBranchUrlFromDeepview(html); + + expect(result).toBeUndefined(); + }); + + it('returns undefined when link_click_id format does not match link-{digits}-{base64}', () => { + const html = buildDeepviewHtml('malformed-id-without-digits'); + + const result = extractBranchUrlFromDeepview(html); + + expect(result).toBeUndefined(); + }); +}); + describe('rewriteBranchUri error handling', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index 566d4828b731..f4391fa7a785 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -79,12 +79,60 @@ function extractDeepviewPath(html: string): string | undefined { return undefined; } +/** + * Extracts the original deep link URL from Branch Deepview HTML by decoding the + * `link_click_id` parameter embedded in the `al:ios:url` meta tag. The + * `link_click_id` value has the format `link-{digits}-{base64_json}`, where the + * base64 JSON payload contains `+url` — the full original URL configured in the + * Branch dashboard for this link. + */ +export function extractBranchUrlFromDeepview(html: string): string | undefined { + const metaMatch = html.match( + / { + if (!branchParsed.searchParams.has(key)) { + branchParsed.searchParams.set(key, value); + } + }); + return branchParsed.toString(); + } catch { + return branchUrl; + } + } + const deepLinkPathMatch = body.match(/\$deeplink_path['":\s]+['"]([^'"]+)['"]/) ?? body.match(/deeplink_path['":\s]+['"]([^'"]+)['"]/); From bc4379460bd953c8e482b9c60567cf41cecbade9 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 19 Mar 2026 23:09:27 +0000 Subject: [PATCH 18/23] [skip ci] Bump version number to 4108 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9c53e81a0d4c..bcc4acc42fc0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.71.0" - versionCode 4099 + versionCode 4108 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 25ceadddc959..28918fad459c 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3534,13 +3534,13 @@ app: VERSION_NAME: 7.71.0 - opts: is_expand: false - VERSION_NUMBER: 4099 + VERSION_NUMBER: 4108 - opts: is_expand: false FLASK_VERSION_NAME: 7.71.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 4099 + FLASK_VERSION_NUMBER: 4108 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 5caa454d5a26..374fbe9cacc2 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 4108; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 4108; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 4108; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 4108; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 4108; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 4108; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 6d59a2402d8fcdd1a39b8641780c15d09f8035a1 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Fri, 20 Mar 2026 13:26:42 +0000 Subject: [PATCH 19/23] feat: revert version numbers --- android/app/build.gradle | 2 +- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9c53e81a0d4c..97165e049447 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.71.0" - versionCode 4099 + versionCode 3607 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 5caa454d5a26..6726013ffa4b 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 3911; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 3911; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 3911; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 3911; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 3911; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4099; + CURRENT_PROJECT_VERSION = 3911; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From f0418e4c9b53894acb08b7777f7f5461b9da3319 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Fri, 20 Mar 2026 14:13:29 +0000 Subject: [PATCH 20/23] feat: fix lint --- app/core/DeeplinkManager/DeeplinkManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index f4391fa7a785..fbc820dbc64b 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -9,7 +9,7 @@ import FCMService from '../../util/notifications/services/FCMService'; import AppConstants from '../AppConstants'; import { BranchParams } from './types/deepLinkAnalytics.types'; -const BRANCH_DOMAIN_HOSTS = [ +const BRANCH_DOMAIN_HOSTS: readonly string[] = [ AppConstants.MM_UNIVERSAL_LINK_HOST, AppConstants.MM_UNIVERSAL_LINK_HOST_ALTERNATE, ]; From d09c731b31dc91f0944c5cbc1010a03bc2e79196 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Fri, 20 Mar 2026 14:52:39 +0000 Subject: [PATCH 21/23] feat: timeout in finally block --- app/core/DeeplinkManager/DeeplinkManager.ts | 94 +++++++++++---------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index fbc820dbc64b..d69dd889084b 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -143,62 +143,64 @@ export async function resolveBranchShortLink( const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3000); - const response = await fetch(cleanUrl, { - redirect: 'follow', - headers: { - 'User-Agent': 'facebookexternalhit/1.1 (MetaMask-LinkResolver)', - }, - signal: controller.signal, - }); - - clearTimeout(timeout); - - const finalUrl = response.url; - try { - const finalHostname = new URL(finalUrl).hostname; - if ( - finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_HOST || - finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_TEST_HOST - ) { - return finalUrl; - } - } catch { - // ignore URL parse errors on finalUrl - } + const response = await fetch(cleanUrl, { + redirect: 'follow', + headers: { + 'User-Agent': 'facebookexternalhit/1.1 (MetaMask-LinkResolver)', + }, + signal: controller.signal, + }); - const body = await response.text(); + const finalUrl = response.url; - const branchUrl = extractBranchUrlFromDeepview(body); - if (branchUrl) { try { - const cleanParsed = new URL(cleanUrl); - const branchParsed = new URL(branchUrl); - cleanParsed.searchParams.forEach((value, key) => { - if (!branchParsed.searchParams.has(key)) { - branchParsed.searchParams.set(key, value); - } - }); - return branchParsed.toString(); + const finalHostname = new URL(finalUrl).hostname; + if ( + finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_HOST || + finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_TEST_HOST + ) { + return finalUrl; + } } catch { - return branchUrl; + // ignore URL parse errors on finalUrl } - } - const deepLinkPathMatch = - body.match(/\$deeplink_path['":\s]+['"]([^'"]+)['"]/) ?? - body.match(/deeplink_path['":\s]+['"]([^'"]+)['"]/); + const body = await response.text(); - if (deepLinkPathMatch?.[1]) { - const path = deepLinkPathMatch[1]; - return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${path.replace(/^\//, '')}`; - } + const branchUrl = extractBranchUrlFromDeepview(body); + if (branchUrl) { + try { + const cleanParsed = new URL(cleanUrl); + const branchParsed = new URL(branchUrl); + cleanParsed.searchParams.forEach((value, key) => { + if (!branchParsed.searchParams.has(key)) { + branchParsed.searchParams.set(key, value); + } + }); + return branchParsed.toString(); + } catch { + return branchUrl; + } + } - const deepviewPath = extractDeepviewPath(body); - if (deepviewPath) { - return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${deepviewPath.replace(/^\//, '')}`; + const deepLinkPathMatch = + body.match(/\$deeplink_path['":\s]+['"]([^'"]+)['"]/) ?? + body.match(/deeplink_path['":\s]+['"]([^'"]+)['"]/); + + if (deepLinkPathMatch?.[1]) { + const path = deepLinkPathMatch[1]; + return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${path.replace(/^\//, '')}`; + } + + const deepviewPath = extractDeepviewPath(body); + if (deepviewPath) { + return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${deepviewPath.replace(/^\//, '')}`; + } + return undefined; + } finally { + clearTimeout(timeout); } - return undefined; } catch (error) { Logger.error( error as Error, From 5994941e2772a2d7a89a880c56cca14bbb8e43c1 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Fri, 20 Mar 2026 15:23:46 +0000 Subject: [PATCH 22/23] feat : bugbot --- app/core/DeeplinkManager/DeeplinkManager.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index d69dd889084b..803cd3ce7c3d 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -155,12 +155,18 @@ export async function resolveBranchShortLink( const finalUrl = response.url; try { - const finalHostname = new URL(finalUrl).hostname; + const finalParsed = new URL(finalUrl); if ( - finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_HOST || - finalHostname === AppConstants.MM_IO_UNIVERSAL_LINK_TEST_HOST + finalParsed.hostname === AppConstants.MM_IO_UNIVERSAL_LINK_HOST || + finalParsed.hostname === AppConstants.MM_IO_UNIVERSAL_LINK_TEST_HOST ) { - return finalUrl; + const cleanParsed = new URL(cleanUrl); + cleanParsed.searchParams.forEach((value, key) => { + if (!finalParsed.searchParams.has(key)) { + finalParsed.searchParams.set(key, value); + } + }); + return finalParsed.toString(); } } catch { // ignore URL parse errors on finalUrl From e7ecd631aa0aca1380135099b9286dfd4034d080 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Fri, 20 Mar 2026 17:28:09 +0000 Subject: [PATCH 23/23] feat: addressing cursor bot comment --- app/core/DeeplinkManager/DeeplinkManager.ts | 26 ++++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index 803cd3ce7c3d..b74d2c982439 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -174,20 +174,24 @@ export async function resolveBranchShortLink( const body = await response.text(); - const branchUrl = extractBranchUrlFromDeepview(body); - if (branchUrl) { + const mergeCleanParams = (resolvedUrl: string): string => { try { + const resolved = new URL(resolvedUrl); const cleanParsed = new URL(cleanUrl); - const branchParsed = new URL(branchUrl); cleanParsed.searchParams.forEach((value, key) => { - if (!branchParsed.searchParams.has(key)) { - branchParsed.searchParams.set(key, value); + if (!resolved.searchParams.has(key)) { + resolved.searchParams.set(key, value); } }); - return branchParsed.toString(); + return resolved.toString(); } catch { - return branchUrl; + return resolvedUrl; } + }; + + const branchUrl = extractBranchUrlFromDeepview(body); + if (branchUrl) { + return mergeCleanParams(branchUrl); } const deepLinkPathMatch = @@ -196,12 +200,16 @@ export async function resolveBranchShortLink( if (deepLinkPathMatch?.[1]) { const path = deepLinkPathMatch[1]; - return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${path.replace(/^\//, '')}`; + return mergeCleanParams( + `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${path.replace(/^\//, '')}`, + ); } const deepviewPath = extractDeepviewPath(body); if (deepviewPath) { - return `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${deepviewPath.replace(/^\//, '')}`; + return mergeCleanParams( + `https://${AppConstants.MM_IO_UNIVERSAL_LINK_HOST}/${deepviewPath.replace(/^\//, '')}`, + ); } return undefined; } finally {