Skip to content

Commit 492b056

Browse files
feat!: Run a WebView per Snap on mobile (#3085)
Allows us to run Snaps locally on mobile by instantiating a WebView per Snap. This PR changes the `WebViewExecutionService` to let it instantiate and remove webviews (this will need support on the mobile implementation side). Additionally alters the WebView bundle so it no longer functions as a "proxy executor", but instead inherits from the iframe executor implementation. **Breaking changes** - `WebViewExecutionService` now requires `createWebView` and `removeWebView` constructor arguments, `getWebView` is no longer supported. - The WebView bundle in `snaps-execution-environments` no longer supports proxy executor calls and functions as a single executor.
1 parent d7e2167 commit 492b056

File tree

7 files changed

+116
-77
lines changed

7 files changed

+116
-77
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"branches": 93.31,
3-
"functions": 96.8,
4-
"lines": 98.15,
5-
"statements": 97.88
3+
"functions": 97.05,
4+
"lines": 98.2,
5+
"statements": 97.93
66
}

packages/snaps-controllers/src/services/webview/WebViewExecutionService.test.ts

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,19 @@ import type { WebViewInterface } from './WebViewMessageStream';
1414
* Create a response message for the given request. This function assumes that
1515
* the response is for the parent, and uses the command stream.
1616
*
17-
* @param message - The request message.
1817
* @param request - The request to respond to.
1918
* @param response - The response to send.
2019
* @returns The response message.
2120
*/
22-
function getResponse(
23-
message: Record<string, unknown>,
24-
request: JsonRpcRequest,
25-
response: Json,
26-
) {
21+
function getResponse(request: JsonRpcRequest, response: Json) {
2722
return {
2823
target: 'parent',
2924
data: {
30-
jobId: message.jobId,
31-
frameUrl: message.frameUrl,
25+
name: 'command',
3226
data: {
33-
name: 'command',
34-
data: {
35-
jsonrpc: '2.0',
36-
id: request.id,
37-
result: response,
38-
},
27+
jsonrpc: '2.0',
28+
id: request.id,
29+
result: response,
3930
},
4031
},
4132
};
@@ -50,11 +41,13 @@ describe('WebViewExecutionService', () => {
5041
};
5142

5243
const { service } = createService(WebViewExecutionService, {
53-
getWebView: async () =>
54-
Promise.resolve(mockedWebView as unknown as WebViewInterface),
44+
createWebView: async (_id: string) =>
45+
mockedWebView as unknown as WebViewInterface,
46+
removeWebView: jest.fn(),
5547
});
5648

5749
expect(service).toBeDefined();
50+
await service.terminateAllSnaps();
5851
});
5952

6053
it('can execute snaps', async () => {
@@ -89,30 +82,29 @@ describe('WebViewExecutionService', () => {
8982
// Handle incoming requests.
9083
if (
9184
isPlainObject(data) &&
92-
isPlainObject(data.data) &&
93-
data.data.name === 'command' &&
94-
isJsonRpcRequest(data.data.data)
85+
data.name === 'command' &&
86+
isJsonRpcRequest(data.data)
9587
) {
96-
const request = data.data.data;
88+
const request = data.data;
9789

9890
// Respond "OK" to the `ping`, `executeSnap`, and `terminate` request.
9991
if (
10092
request.method === 'ping' ||
10193
request.method === 'executeSnap' ||
10294
request.method === 'terminate'
10395
) {
104-
sendMessage(getResponse(data, request, 'OK'));
96+
sendMessage(getResponse(request, 'OK'));
10597
}
10698
}
10799
});
108100

109101
const { service } = createService(WebViewExecutionService, {
110-
getWebView: async () =>
111-
Promise.resolve({
112-
registerMessageListener,
113-
unregisterMessageListener: jest.fn(),
114-
injectJavaScript,
115-
}),
102+
createWebView: async (_id: string) => ({
103+
registerMessageListener,
104+
unregisterMessageListener: jest.fn(),
105+
injectJavaScript,
106+
}),
107+
removeWebView: jest.fn(),
116108
});
117109

118110
expect(
@@ -122,5 +114,7 @@ describe('WebViewExecutionService', () => {
122114
endowments: [],
123115
}),
124116
).toBe('OK');
117+
118+
await service.terminateAllSnaps();
125119
});
126120
});
Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,35 @@
1-
import type { ExecutionServiceArgs } from '../AbstractExecutionService';
2-
import { ProxyExecutionService } from '../proxy/ProxyExecutionService';
1+
import type { TerminateJobArgs } from '../AbstractExecutionService';
2+
import {
3+
AbstractExecutionService,
4+
type ExecutionServiceArgs,
5+
} from '../AbstractExecutionService';
36
import type { WebViewInterface } from './WebViewMessageStream';
47
import { WebViewMessageStream } from './WebViewMessageStream';
58

69
export type WebViewExecutionServiceArgs = ExecutionServiceArgs & {
7-
getWebView: () => Promise<WebViewInterface>;
10+
createWebView: (jobId: string) => Promise<WebViewInterface>;
11+
removeWebView: (jobId: string) => void;
812
};
913

10-
export class WebViewExecutionService extends ProxyExecutionService {
11-
#getWebView;
14+
export class WebViewExecutionService extends AbstractExecutionService<WebViewInterface> {
15+
readonly #createWebView;
16+
17+
readonly #removeWebView;
1218

1319
constructor({
1420
messenger,
1521
setupSnapProvider,
16-
getWebView,
22+
createWebView,
23+
removeWebView,
1724
...args
1825
}: WebViewExecutionServiceArgs) {
1926
super({
2027
...args,
2128
messenger,
2229
setupSnapProvider,
23-
stream: new WebViewMessageStream({
24-
name: 'parent',
25-
target: 'child',
26-
getWebView,
27-
}),
2830
});
29-
this.#getWebView = getWebView;
31+
this.#createWebView = createWebView;
32+
this.#removeWebView = removeWebView;
3033
}
3134

3235
/**
@@ -37,16 +40,18 @@ export class WebViewExecutionService extends ProxyExecutionService {
3740
* @returns An object with the worker ID and stream.
3841
*/
3942
protected async initEnvStream(jobId: string) {
40-
// Ensure that the WebView has been loaded before we proceed.
41-
await this.#ensureWebViewLoaded();
43+
const webView = await this.#createWebView(jobId);
44+
45+
const stream = new WebViewMessageStream({
46+
name: 'parent',
47+
target: 'child',
48+
webView,
49+
});
4250

43-
return super.initEnvStream(jobId);
51+
return { worker: webView, stream };
4452
}
4553

46-
/**
47-
* Ensure that the WebView has been loaded by awaiting the getWebView promise.
48-
*/
49-
async #ensureWebViewLoaded() {
50-
await this.#getWebView();
54+
protected terminateJob(jobWrapper: TerminateJobArgs<WebViewInterface>): void {
55+
this.#removeWebView(jobWrapper.id);
5156
}
5257
}

packages/snaps-controllers/src/services/webview/WebViewMessageStream.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { PostMessageEvent } from '@metamask/post-message-stream';
22
import { BasePostMessageStream } from '@metamask/post-message-stream';
33
import { isValidStreamMessage } from '@metamask/post-message-stream/dist/utils';
4-
import { logError } from '@metamask/snaps-utils';
54
import { assert, stringToBytes } from '@metamask/utils';
65

76
export type WebViewInterface = {
@@ -13,7 +12,7 @@ export type WebViewInterface = {
1312
export type WebViewStreamArgs = {
1413
name: string;
1514
target: string;
16-
getWebView: () => Promise<WebViewInterface>;
15+
webView: WebViewInterface;
1716
};
1817

1918
/**
@@ -33,29 +32,21 @@ export class WebViewMessageStream extends BasePostMessageStream {
3332
* @param args.name - The name of the stream. Used to differentiate between
3433
* multiple streams sharing the same window object.
3534
* @param args.target - The name of the stream to exchange messages with.
36-
* @param args.getWebView - A asynchronous getter for the webview.
35+
* @param args.webView - A reference to the WebView.
3736
*/
38-
constructor({ name, target, getWebView }: WebViewStreamArgs) {
37+
constructor({ name, target, webView }: WebViewStreamArgs) {
3938
super();
4039

4140
this.#name = name;
4241
this.#target = target;
4342

4443
this._onMessage = this._onMessage.bind(this);
4544

46-
// This is a bit atypical from other post-message streams.
47-
// We have to wait for the WebView to fully load before we can continue using the stream.
48-
getWebView()
49-
.then((webView) => {
50-
this.#webView = webView;
51-
// This method is already bound.
52-
// eslint-disable-next-line @typescript-eslint/unbound-method
53-
webView.registerMessageListener(this._onMessage);
54-
this._handshake();
55-
})
56-
.catch((error) => {
57-
logError(error);
58-
});
45+
this.#webView = webView;
46+
// This method is already bound.
47+
// eslint-disable-next-line @typescript-eslint/unbound-method
48+
this.#webView.registerMessageListener(this._onMessage);
49+
this._handshake();
5950
}
6051

6152
protected _postMessage(data: unknown): void {

packages/snaps-controllers/src/test-utils/webview.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function parseInjectedJS(js: string) {
1717
/**
1818
* Takes no param and return mocks necessary for testing WebViewMessageStream.
1919
*
20-
* @returns The mockWebView, mockGetWebView, and mockStream.
20+
* @returns The mockWebView, and mockStream.
2121
*/
2222
export function createWebViewObjects() {
2323
const registerMessageListenerA = jest.fn();
@@ -45,26 +45,21 @@ export function createWebViewObjects() {
4545
}),
4646
};
4747

48-
const mockGetWebViewA = jest.fn().mockResolvedValue(mockWebViewA);
49-
const mockGetWebViewB = jest.fn().mockResolvedValue(mockWebViewB);
50-
5148
const streamA = new WebViewMessageStream({
5249
name: 'a',
5350
target: 'b',
54-
getWebView: mockGetWebViewA,
51+
webView: mockWebViewA,
5552
});
5653

5754
const streamB = new WebViewMessageStream({
5855
name: 'b',
5956
target: 'a',
60-
getWebView: mockGetWebViewB,
57+
webView: mockWebViewB,
6158
});
6259

6360
return {
6461
mockWebViewA,
6562
mockWebViewB,
66-
mockGetWebViewA,
67-
mockGetWebViewB,
6863
streamA,
6964
streamB,
7065
};

packages/snaps-execution-environments/lavamoat/browserify/webview/policy.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
{
22
"resources": {
3+
"@metamask/json-rpc-engine": {
4+
"packages": {
5+
"@metamask/providers>@metamask/safe-event-emitter": true,
6+
"@metamask/rpc-errors": true,
7+
"@metamask/utils": true
8+
}
9+
},
10+
"@metamask/object-multiplex": {
11+
"globals": {
12+
"console.warn": true
13+
},
14+
"packages": {
15+
"@metamask/object-multiplex>once": true,
16+
"readable-stream": true
17+
}
18+
},
19+
"@metamask/object-multiplex>once": {
20+
"packages": {
21+
"@metamask/object-multiplex>once>wrappy": true
22+
}
23+
},
324
"@metamask/post-message-stream": {
425
"globals": {
526
"MessageEvent.prototype": true,
@@ -16,6 +37,39 @@
1637
"readable-stream": true
1738
}
1839
},
40+
"@metamask/providers": {
41+
"globals": {
42+
"console": true
43+
},
44+
"packages": {
45+
"@metamask/json-rpc-engine": true,
46+
"@metamask/providers>@metamask/json-rpc-middleware-stream": true,
47+
"@metamask/providers>@metamask/safe-event-emitter": true,
48+
"@metamask/providers>is-stream": true,
49+
"@metamask/rpc-errors": true,
50+
"eslint>fast-deep-equal": true,
51+
"readable-stream": true
52+
}
53+
},
54+
"@metamask/providers>@metamask/json-rpc-middleware-stream": {
55+
"globals": {
56+
"console.warn": true,
57+
"setTimeout": true
58+
},
59+
"packages": {
60+
"@metamask/providers>@metamask/safe-event-emitter": true,
61+
"@metamask/utils": true,
62+
"readable-stream": true
63+
}
64+
},
65+
"@metamask/providers>@metamask/safe-event-emitter": {
66+
"globals": {
67+
"setTimeout": true
68+
},
69+
"packages": {
70+
"browserify>events": true
71+
}
72+
},
1973
"@metamask/rpc-errors": {
2074
"packages": {
2175
"@metamask/rpc-errors>fast-safe-stringify": true,

packages/snaps-execution-environments/src/webview/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { executeLockdownEvents } from '../common/lockdown/lockdown-events';
22
import { executeLockdownMore } from '../common/lockdown/lockdown-more';
3-
import { ProxySnapExecutor } from '../proxy/ProxySnapExecutor';
3+
import { IFrameSnapExecutor } from '../iframe/IFrameSnapExecutor';
44
import { WebViewExecutorStream } from './WebViewExecutorStream';
55

66
// Lockdown is already applied in LavaMoat
@@ -13,4 +13,4 @@ const parentStream = new WebViewExecutorStream({
1313
targetWindow: window.ReactNativeWebView,
1414
});
1515

16-
ProxySnapExecutor.initialize(parentStream);
16+
IFrameSnapExecutor.initialize(parentStream);

0 commit comments

Comments
 (0)