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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface RemoteCommunicationPostMessageStreamState {
_name: any;
remote: RemoteCommunication | null;
deeplinkProtocol: boolean;
hideReturnToAppNotification?: boolean;
platformManager: PlatformManager | null;
}

Expand All @@ -25,17 +26,20 @@ export class RemoteCommunicationPostMessageStream
_name: null,
remote: null,
deeplinkProtocol: false,
hideReturnToAppNotification: false,
platformManager: null,
};

constructor({
name,
remote,
deeplinkProtocol,
hideReturnToAppNotification,
platformManager,
}: {
name: ProviderConstants;
deeplinkProtocol: boolean;
hideReturnToAppNotification?: boolean;
remote: RemoteCommunication;
platformManager: PlatformManager;
}) {
Expand All @@ -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);
Expand Down
34 changes: 34 additions & 0 deletions packages/sdk/src/PostMessageStream/getPostMessageStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
});
2 changes: 2 additions & 0 deletions packages/sdk/src/PostMessageStream/getPostMessageStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export const getPostMessageStream = ({
name,
remote: remoteConnection?.getConnector(),
deeplinkProtocol: remoteConnection?.state.deeplinkProtocol,
hideReturnToAppNotification:
remoteConnection?.state.hideReturnToAppNotification,
platformManager: remoteConnection?.getPlatformManager(),
});
};
27 changes: 27 additions & 0 deletions packages/sdk/src/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

it('should set max listeners', () => {
expect(sdk.getMaxListeners()).toBe(50);
});
Expand Down
75 changes: 42 additions & 33 deletions packages/sdk/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -210,58 +242,35 @@ 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');
}
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 {
throw new Error(`You must provide dAppMetadata url`);
}
}

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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ describe('performSDKInitialization', () => {
injectProvider: true,
shouldShimWeb3: true,
useDeeplink: true,
hideReturnToAppNotification: false,
storage: {
enabled: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading