diff --git a/packages/sdk/src/PostMessageStream/RemoteCommunicationPostMessageStream.test.ts b/packages/sdk/src/PostMessageStream/RemoteCommunicationPostMessageStream.test.ts index 96475c678..5148d9741 100644 --- a/packages/sdk/src/PostMessageStream/RemoteCommunicationPostMessageStream.test.ts +++ b/packages/sdk/src/PostMessageStream/RemoteCommunicationPostMessageStream.test.ts @@ -36,6 +36,22 @@ describe('RemoteCommunicationPostMessageStream', () => { ); }); + it('should initialize with hideReturnToAppNotification option', () => { + const instanceWithOption = new RemoteCommunicationPostMessageStream({ + name: ProviderConstants.PROVIDER, + remote: mockRemoteCommunication, + deeplinkProtocol: false, + platformManager: mockPlatformManager, + hideReturnToAppNotification: true, + }); + + expect(instanceWithOption.state.hideReturnToAppNotification).toBe(true); + }); + + it('should have hideReturnToAppNotification as false by default', () => { + expect(instance.state.hideReturnToAppNotification).toBe(false); + }); + it('should call _write properly', async () => { const chunk = 'someData'; const encoding = 'utf8'; diff --git a/packages/sdk/src/PostMessageStream/RemoteCommunicationPostMessageStream.ts b/packages/sdk/src/PostMessageStream/RemoteCommunicationPostMessageStream.ts index 724ba4018..3b6a90f82 100644 --- a/packages/sdk/src/PostMessageStream/RemoteCommunicationPostMessageStream.ts +++ b/packages/sdk/src/PostMessageStream/RemoteCommunicationPostMessageStream.ts @@ -14,6 +14,7 @@ interface RemoteCommunicationPostMessageStreamState { _name: any; remote: RemoteCommunication | null; deeplinkProtocol: boolean; + hideReturnToAppNotification?: boolean; platformManager: PlatformManager | null; } @@ -25,6 +26,7 @@ export class RemoteCommunicationPostMessageStream _name: null, remote: null, deeplinkProtocol: false, + hideReturnToAppNotification: false, platformManager: null, }; @@ -32,10 +34,12 @@ export class RemoteCommunicationPostMessageStream name, remote, deeplinkProtocol, + hideReturnToAppNotification, platformManager, }: { name: ProviderConstants; deeplinkProtocol: boolean; + hideReturnToAppNotification?: boolean; remote: RemoteCommunication; platformManager: PlatformManager; }) { @@ -45,6 +49,8 @@ export class RemoteCommunicationPostMessageStream this.state._name = name; this.state.remote = remote; this.state.deeplinkProtocol = deeplinkProtocol; + this.state.hideReturnToAppNotification = + hideReturnToAppNotification ?? this.state.hideReturnToAppNotification; this.state.platformManager = platformManager; this._onMessage = this._onMessage.bind(this); diff --git a/packages/sdk/src/PostMessageStream/getPostMessageStream.test.ts b/packages/sdk/src/PostMessageStream/getPostMessageStream.test.ts index f1a969c36..237aa5120 100644 --- a/packages/sdk/src/PostMessageStream/getPostMessageStream.test.ts +++ b/packages/sdk/src/PostMessageStream/getPostMessageStream.test.ts @@ -59,4 +59,38 @@ describe('getPostMessageStream', () => { platformManager: fakePlatformManager, }); }); + + it('should pass hideReturnToAppNotification from remoteConnection state', () => { + const fakeConnector = {}; + const fakePlatformManager = {}; + const hideReturnToAppNotification = true; + mockRemoteConnection.mockImplementation( + () => + ({ + getConnector: jest.fn().mockReturnValue(fakeConnector), + getPlatformManager: jest.fn().mockReturnValue(fakePlatformManager), + state: { + deeplinkProtocol: false, + hideReturnToAppNotification, + }, + } as any), + ); + + const result = getPostMessageStream({ + name: ProviderConstants.CONTENT_SCRIPT, + remoteConnection: new RemoteConnection({ + getConnector: jest.fn().mockReturnValue(fakeConnector), + } as any), + debug: false, + } as any); + + expect(result).toBeInstanceOf(RemoteCommunicationPostMessageStream); + expect(mockPostMessageStream).toHaveBeenCalledWith({ + name: ProviderConstants.CONTENT_SCRIPT, + remote: fakeConnector, + deeplinkProtocol: false, + platformManager: fakePlatformManager, + hideReturnToAppNotification, + }); + }); }); diff --git a/packages/sdk/src/PostMessageStream/getPostMessageStream.ts b/packages/sdk/src/PostMessageStream/getPostMessageStream.ts index 3570f92e6..52b76ad67 100644 --- a/packages/sdk/src/PostMessageStream/getPostMessageStream.ts +++ b/packages/sdk/src/PostMessageStream/getPostMessageStream.ts @@ -25,6 +25,8 @@ export const getPostMessageStream = ({ name, remote: remoteConnection?.getConnector(), deeplinkProtocol: remoteConnection?.state.deeplinkProtocol, + hideReturnToAppNotification: + remoteConnection?.state.hideReturnToAppNotification, platformManager: remoteConnection?.getPlatformManager(), }); }; diff --git a/packages/sdk/src/sdk.test.ts b/packages/sdk/src/sdk.test.ts index 49cf5a121..78b1cf5ef 100644 --- a/packages/sdk/src/sdk.test.ts +++ b/packages/sdk/src/sdk.test.ts @@ -199,6 +199,33 @@ describe('MetaMaskSDK', () => { expect(sdk.options).toMatchObject(options); }); + it('should initialize with hideReturnToAppNotification option', () => { + const options: MetaMaskSDKOptions = { + hideReturnToAppNotification: true, + dappMetadata: { + name: 'Test DApp', + url: 'http://test-dapp.com', + }, + }; + + sdk = new MetaMaskSDK(options); + + expect(sdk.options.hideReturnToAppNotification).toBe(true); + }); + + it('should have hideReturnToAppNotification as false by default', () => { + const options: MetaMaskSDKOptions = { + dappMetadata: { + name: 'Test DApp', + url: 'http://test-dapp.com', + }, + }; + + sdk = new MetaMaskSDK(options); + + expect(sdk.options.hideReturnToAppNotification).toBe(false); + }); + it('should set max listeners', () => { expect(sdk.getMaxListeners()).toBe(50); }); diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/sdk.ts index b42308f20..1c26402ac 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/sdk.ts @@ -91,6 +91,12 @@ export interface MetaMaskSDKOptions { */ useDeeplink?: boolean; + /** + * If true, MetaMask Mobile will hide the modal that invites the user to return to the app after performing an action on MetaMask Mobile. + * This should be used when the app is loaded in an iframe inside the MetaMask Mobile browser. + */ + hideReturnToAppNotification?: boolean; + /** * If true, the SDK will shim the window.web3 object with the provider returned by the SDK (useful for compatibility with older browser). */ @@ -175,6 +181,32 @@ export interface MetaMaskSDKOptions { }; } +/** + * Default options for the MetaMask SDK. + */ +export const defaultMetaMaskSDKOptions: MetaMaskSDKOptions = { + storage: { + enabled: true, + }, + injectProvider: true, + forceInjectProvider: false, + enableAnalytics: true, + shouldShimWeb3: true, + useDeeplink: true, + hideReturnToAppNotification: false, + extensionOnly: true, + headless: false, + dappMetadata: { + name: '', + url: '', + iconUrl: '', + }, + _source: DEFAULT_SDK_SOURCE, + i18nOptions: { + enabled: false, + }, +}; + export class MetaMaskSDK extends EventEmitter2 { public options: MetaMaskSDKOptions; @@ -210,34 +242,15 @@ export class MetaMaskSDK extends EventEmitter2 { private readonly ANON_ID_STORAGE_KEY = 'mm-sdk-anon-id'; - constructor( - options: MetaMaskSDKOptions = { - storage: { - enabled: true, - }, - injectProvider: true, - forceInjectProvider: false, - enableAnalytics: true, - shouldShimWeb3: true, - useDeeplink: true, - extensionOnly: true, - headless: false, - dappMetadata: { - name: '', - url: '', - iconUrl: '', - }, - _source: DEFAULT_SDK_SOURCE, - i18nOptions: { - enabled: false, - }, - }, - ) { + constructor(options?: MetaMaskSDKOptions) { super(); debug.disable(); // initially disabled - const developerMode = options.logging?.developerMode === true; - const debugEnabled = options.logging?.sdk || developerMode; + // Merge user options with default options + this.options = { ...defaultMetaMaskSDKOptions, ...options }; + + const developerMode = this.options.logging?.developerMode === true; + const debugEnabled = this.options.logging?.sdk || developerMode; if (debugEnabled) { debug.enable('MM_SDK'); @@ -245,11 +258,12 @@ export class MetaMaskSDK extends EventEmitter2 { logger(`[MetaMaskSDK: constructor()]: begin.`); this.setMaxListeners(50); - if (!options.dappMetadata?.url) { + // If dappMetadata.url is undefined or empty string, try to set it automatically for web environments + if (!this.options.dappMetadata?.url) { // Automatically set dappMetadata on web env. if (typeof window !== 'undefined' && typeof document !== 'undefined') { - options.dappMetadata = { - ...options.dappMetadata, + this.options.dappMetadata = { + ...this.options.dappMetadata, url: `${window.location.protocol}//${window.location.host}`, }; } else { @@ -257,11 +271,6 @@ export class MetaMaskSDK extends EventEmitter2 { } } - this.options = options; - if (!this.options._source) { - options._source = DEFAULT_SDK_SOURCE; - } - // Automatically initialize the SDK to keep the same behavior as before this.init() .then(() => { diff --git a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.test.ts b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.test.ts index ea16845eb..eeef02d26 100644 --- a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.test.ts +++ b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.test.ts @@ -57,6 +57,7 @@ describe('performSDKInitialization', () => { injectProvider: true, shouldShimWeb3: true, useDeeplink: true, + hideReturnToAppNotification: false, storage: { enabled: true, }, diff --git a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.ts b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.ts index 2e1e2ea84..5358cd3fb 100644 --- a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.ts +++ b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.ts @@ -57,6 +57,9 @@ export async function performSDKInitialization(instance: MetaMaskSDK) { options.shouldShimWeb3 = options.shouldShimWeb3 ?? true; options.extensionOnly = options.extensionOnly ?? true; options.useDeeplink = options.useDeeplink ?? true; + options.hideReturnToAppNotification = + options.hideReturnToAppNotification ?? false; + options.storage = options.storage ?? { enabled: true, }; diff --git a/packages/sdk/src/services/RemoteCommunicationPostMessageStream/write.test.ts b/packages/sdk/src/services/RemoteCommunicationPostMessageStream/write.test.ts index c5a096726..e3ea92b83 100644 --- a/packages/sdk/src/services/RemoteCommunicationPostMessageStream/write.test.ts +++ b/packages/sdk/src/services/RemoteCommunicationPostMessageStream/write.test.ts @@ -317,4 +317,83 @@ describe('write function', () => { expect(mockSendMessage).toHaveBeenCalled(); }); }); + + describe('hideReturnToAppNotification URL parameter', () => { + beforeEach(() => { + mockIsReady.mockReturnValue(true); + mockIsConnected.mockReturnValue(true); + mockIsAuthorized.mockReturnValue(true); + mockIsMobileWeb.mockReturnValue(false); + mockIsSecure.mockReturnValue(true); + mockGetChannelId.mockReturnValue('some_channel_id'); + mockIsMetaMaskInstalled.mockReturnValue(true); + mockGetKeyInfo.mockReturnValue({ ecies: { public: 'test_public_key' } }); + mockHasDeeplinkProtocol.mockReturnValue(false); + mockExtractMethod.mockReturnValue({ + method: Object.keys(METHODS_TO_REDIRECT)[0], + data: { + data: { + jsonrpc: '2.0', + method: Object.keys(METHODS_TO_REDIRECT)[0], + params: [], + }, + }, + triggeredInstaller: false, + }); + }); + + it('should include hr=1 in URL when hideReturnToAppNotification is true', async () => { + mockRemoteCommunicationPostMessageStream.state.hideReturnToAppNotification = + true; + + await write( + mockRemoteCommunicationPostMessageStream, + { jsonrpc: '2.0', method: Object.keys(METHODS_TO_REDIRECT)[0] }, + 'utf8', + callback, + ); + + expect(mockOpenDeeplink).toHaveBeenCalledWith( + expect.stringContaining('hr=1'), + expect.stringContaining('hr=1'), + '_self', + ); + }); + + it('should include hr=0 in URL when hideReturnToAppNotification is false', async () => { + mockRemoteCommunicationPostMessageStream.state.hideReturnToAppNotification = + false; + + await write( + mockRemoteCommunicationPostMessageStream, + { jsonrpc: '2.0', method: Object.keys(METHODS_TO_REDIRECT)[0] }, + 'utf8', + callback, + ); + + expect(mockOpenDeeplink).toHaveBeenCalledWith( + expect.stringContaining('hr=0'), + expect.stringContaining('hr=0'), + '_self', + ); + }); + + it('should include hr=0 in URL when hideReturnToAppNotification is undefined', async () => { + mockRemoteCommunicationPostMessageStream.state.hideReturnToAppNotification = + undefined; + + await write( + mockRemoteCommunicationPostMessageStream, + { jsonrpc: '2.0', method: Object.keys(METHODS_TO_REDIRECT)[0] }, + 'utf8', + callback, + ); + + expect(mockOpenDeeplink).toHaveBeenCalledWith( + expect.stringContaining('hr=0'), + expect.stringContaining('hr=0'), + '_self', + ); + }); + }); }); diff --git a/packages/sdk/src/services/RemoteCommunicationPostMessageStream/write.ts b/packages/sdk/src/services/RemoteCommunicationPostMessageStream/write.ts index 0ca239ec8..5b4d34ea9 100644 --- a/packages/sdk/src/services/RemoteCommunicationPostMessageStream/write.ts +++ b/packages/sdk/src/services/RemoteCommunicationPostMessageStream/write.ts @@ -100,7 +100,9 @@ export async function write( const pubKey = instance.state.remote?.getKeyInfo()?.ecies.public ?? ''; let urlParams = encodeURI( - `channelId=${channelId}&pubkey=${pubKey}&comm=socket&t=d&v=2`, + `channelId=${channelId}&pubkey=${pubKey}&comm=socket&t=d&v=2&hr=${ + instance.state.hideReturnToAppNotification ? 1 : 0 + }`, ); if (activeDeeplinkProtocol) { diff --git a/packages/sdk/src/services/RemoteConnection/ConnectionManager/startConnection.test.ts b/packages/sdk/src/services/RemoteConnection/ConnectionManager/startConnection.test.ts index 38b834478..44f8d812e 100644 --- a/packages/sdk/src/services/RemoteConnection/ConnectionManager/startConnection.test.ts +++ b/packages/sdk/src/services/RemoteConnection/ConnectionManager/startConnection.test.ts @@ -223,4 +223,55 @@ describe('startConnection', () => { ); }); }); + + describe('hideReturnToAppNotification URL parameter', () => { + beforeEach(() => { + mockIsSecure.mockReturnValue(true); + }); + + it('should pass hr=1 when hideReturnToAppNotification is true', async () => { + state.hideReturnToAppNotification = true; + + await startConnection(state, options); + + // Vérifier que le QR code link contient hr=1 + expect(state.qrcodeLink).toContain('hr=1'); + }); + + it('should pass hr=0 when hideReturnToAppNotification is false', async () => { + state.hideReturnToAppNotification = false; + + await startConnection(state, options); + + // Vérifier que le QR code link contient hr=0 + expect(state.qrcodeLink).toContain('hr=0'); + }); + + it('should pass hr=0 when hideReturnToAppNotification is undefined', async () => { + state.hideReturnToAppNotification = undefined; + + await startConnection(state, options); + + // Vérifier que le QR code link contient hr=0 + expect(state.qrcodeLink).toContain('hr=0'); + }); + + it('should include hr parameter in connectWith scenarios', async () => { + mockIsSecure.mockReturnValue(false); + state.hideReturnToAppNotification = true; + const connectWith = { + method: 'eth_sign', + params: ['0x123456', '0xabcdef'], + id: '123', + }; + + await startConnection(state, options, { connectWith }); + + expect(mockConnectWithModalInstaller).toHaveBeenCalledWith( + state, + options, + expect.stringContaining('hr=1'), + ); + }); + }); }); diff --git a/packages/sdk/src/services/RemoteConnection/ConnectionManager/startConnection.ts b/packages/sdk/src/services/RemoteConnection/ConnectionManager/startConnection.ts index 7b2feab03..03d835033 100644 --- a/packages/sdk/src/services/RemoteConnection/ConnectionManager/startConnection.ts +++ b/packages/sdk/src/services/RemoteConnection/ConnectionManager/startConnection.ts @@ -148,7 +148,9 @@ export async function startConnection( let linkParams = `channelId=${channelId}&v=2&comm=${ state.communicationLayerPreference ?? '' - }&pubkey=${pubKey}${qrCodeOrigin}&originatorInfo=${base64OriginatorInfo}`; + }&pubkey=${pubKey}${qrCodeOrigin}&originatorInfo=${base64OriginatorInfo}&hr=${ + state.hideReturnToAppNotification ? 1 : 0 + }`; if (connectWith) { const base64Rpc = base64Encode(JSON.stringify(connectWith)); diff --git a/packages/sdk/src/services/RemoteConnection/RemoteConnection.ts b/packages/sdk/src/services/RemoteConnection/RemoteConnection.ts index 22d4b134c..149d7616e 100644 --- a/packages/sdk/src/services/RemoteConnection/RemoteConnection.ts +++ b/packages/sdk/src/services/RemoteConnection/RemoteConnection.ts @@ -93,6 +93,7 @@ export interface RemoteConnectionState { connector?: RemoteCommunication; qrcodeLink?: string; useDeeplink?: boolean; + hideReturnToAppNotification?: boolean; developerMode: boolean; analytics?: Analytics; authorized: boolean; @@ -146,6 +147,9 @@ export class RemoteConnection { this.state.analytics = options.analytics; this.state.preferDesktop = options.preferDesktop ?? false; this.state.useDeeplink = options.sdk.options.useDeeplink; + this.state.hideReturnToAppNotification = + options.sdk.options.hideReturnToAppNotification; + this.state.communicationLayerPreference = options.communicationLayerPreference; this.state.platformManager = options.platformManager;