Skip to content

Commit 64e3948

Browse files
FrederikBoldingPatrykLucka
authored andcommitted
feat: Add waitForUpdate interface action (#2960)
Adds `waitForUpdate` as an interface action, which lets developers wait for the Snap content to be updated by the Snap. This is useful when waiting for the Snap to populate the UI with information gathered asynchronously. Fixes #2958
1 parent 051673b commit 64e3948

File tree

9 files changed

+159
-2
lines changed

9 files changed

+159
-2
lines changed

packages/snaps-jest/src/helpers.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ describe('installSnap', () => {
411411
selectFromRadioGroup: expect.any(Function),
412412
selectFromSelector: expect.any(Function),
413413
uploadFile: expect.any(Function),
414+
waitForUpdate: expect.any(Function),
414415
ok: expect.any(Function),
415416
cancel: expect.any(Function),
416417
});
@@ -473,6 +474,7 @@ describe('installSnap', () => {
473474
selectFromRadioGroup: expect.any(Function),
474475
selectFromSelector: expect.any(Function),
475476
uploadFile: expect.any(Function),
477+
waitForUpdate: expect.any(Function),
476478
ok: expect.any(Function),
477479
cancel: expect.any(Function),
478480
});
@@ -535,6 +537,7 @@ describe('installSnap', () => {
535537
selectFromRadioGroup: expect.any(Function),
536538
selectFromSelector: expect.any(Function),
537539
uploadFile: expect.any(Function),
540+
waitForUpdate: expect.any(Function),
538541
ok: expect.any(Function),
539542
});
540543

packages/snaps-simulation/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"@metamask/superstruct": "^3.1.0",
7171
"@metamask/utils": "^10.0.0",
7272
"@reduxjs/toolkit": "^1.9.5",
73+
"fast-deep-equal": "^3.1.3",
7374
"mime": "^3.0.0",
7475
"readable-stream": "^3.6.2",
7576
"redux-saga": "^1.2.3"

packages/snaps-simulation/src/controllers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
ExecutionServiceActions,
1616
SnapInterfaceControllerActions,
1717
SnapInterfaceControllerAllowedActions,
18+
SnapInterfaceControllerStateChangeEvent,
1819
} from '@metamask/snaps-controllers';
1920
import {
2021
caveatSpecifications as snapsCaveatsSpecifications,
@@ -38,9 +39,12 @@ export type RootControllerAllowedActions =
3839
| ExecutionServiceActions
3940
| SubjectMetadataControllerActions;
4041

42+
export type RootControllerAllowedEvents =
43+
SnapInterfaceControllerStateChangeEvent;
44+
4145
export type RootControllerMessenger = ControllerMessenger<
4246
RootControllerAllowedActions,
43-
any
47+
RootControllerAllowedEvents
4448
>;
4549

4650
export type GetControllersOptions = {

packages/snaps-simulation/src/helpers.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ describe('helpers', () => {
145145
selectFromRadioGroup: expect.any(Function),
146146
selectFromSelector: expect.any(Function),
147147
uploadFile: expect.any(Function),
148+
waitForUpdate: expect.any(Function),
148149
ok: expect.any(Function),
149150
cancel: expect.any(Function),
150151
});
@@ -207,6 +208,7 @@ describe('helpers', () => {
207208
selectFromRadioGroup: expect.any(Function),
208209
selectFromSelector: expect.any(Function),
209210
uploadFile: expect.any(Function),
211+
waitForUpdate: expect.any(Function),
210212
ok: expect.any(Function),
211213
cancel: expect.any(Function),
212214
});
@@ -269,6 +271,7 @@ describe('helpers', () => {
269271
selectFromRadioGroup: expect.any(Function),
270272
selectFromSelector: expect.any(Function),
271273
uploadFile: expect.any(Function),
274+
waitForUpdate: expect.any(Function),
272275
ok: expect.any(Function),
273276
});
274277

packages/snaps-simulation/src/interface.test.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
typeInField,
5151
uploadFile,
5252
selectFromSelector,
53+
waitForUpdate,
5354
} from './interface';
5455
import type { RunSagaFunction } from './store';
5556
import { createStore, resolveInterface, setInterface } from './store';
@@ -90,6 +91,7 @@ describe('getInterfaceResponse', () => {
9091
selectFromRadioGroup: jest.fn(),
9192
selectFromSelector: jest.fn(),
9293
uploadFile: jest.fn(),
94+
waitForUpdate: jest.fn(),
9395
};
9496

9597
it('returns an `ok` function that resolves the user interface with `null` for alert dialogs', async () => {
@@ -111,6 +113,7 @@ describe('getInterfaceResponse', () => {
111113
selectFromRadioGroup: expect.any(Function),
112114
selectFromSelector: expect.any(Function),
113115
uploadFile: expect.any(Function),
116+
waitForUpdate: expect.any(Function),
114117
ok: expect.any(Function),
115118
});
116119

@@ -138,6 +141,7 @@ describe('getInterfaceResponse', () => {
138141
selectFromRadioGroup: expect.any(Function),
139142
selectFromSelector: expect.any(Function),
140143
uploadFile: expect.any(Function),
144+
waitForUpdate: expect.any(Function),
141145
ok: expect.any(Function),
142146
cancel: expect.any(Function),
143147
});
@@ -166,6 +170,7 @@ describe('getInterfaceResponse', () => {
166170
selectFromRadioGroup: expect.any(Function),
167171
selectFromSelector: expect.any(Function),
168172
uploadFile: expect.any(Function),
173+
waitForUpdate: expect.any(Function),
169174
ok: expect.any(Function),
170175
cancel: expect.any(Function),
171176
});
@@ -194,6 +199,7 @@ describe('getInterfaceResponse', () => {
194199
selectFromRadioGroup: expect.any(Function),
195200
selectFromSelector: expect.any(Function),
196201
uploadFile: expect.any(Function),
202+
waitForUpdate: expect.any(Function),
197203
ok: expect.any(Function),
198204
cancel: expect.any(Function),
199205
});
@@ -222,6 +228,7 @@ describe('getInterfaceResponse', () => {
222228
selectFromRadioGroup: expect.any(Function),
223229
selectFromSelector: expect.any(Function),
224230
uploadFile: expect.any(Function),
231+
waitForUpdate: expect.any(Function),
225232
ok: expect.any(Function),
226233
cancel: expect.any(Function),
227234
});
@@ -250,6 +257,7 @@ describe('getInterfaceResponse', () => {
250257
selectFromRadioGroup: expect.any(Function),
251258
selectFromSelector: expect.any(Function),
252259
uploadFile: expect.any(Function),
260+
waitForUpdate: expect.any(Function),
253261
ok: expect.any(Function),
254262
cancel: expect.any(Function),
255263
});
@@ -296,6 +304,7 @@ describe('getInterfaceResponse', () => {
296304
selectInDropdown: expect.any(Function),
297305
selectFromRadioGroup: expect.any(Function),
298306
selectFromSelector: expect.any(Function),
307+
waitForUpdate: expect.any(Function),
299308
uploadFile: expect.any(Function),
300309
});
301310
});
@@ -336,6 +345,7 @@ describe('getInterfaceResponse', () => {
336345
selectFromRadioGroup: expect.any(Function),
337346
selectFromSelector: expect.any(Function),
338347
uploadFile: expect.any(Function),
348+
waitForUpdate: expect.any(Function),
339349
cancel: expect.any(Function),
340350
});
341351
});
@@ -370,6 +380,7 @@ describe('getInterfaceResponse', () => {
370380
selectFromRadioGroup: expect.any(Function),
371381
selectFromSelector: expect.any(Function),
372382
uploadFile: expect.any(Function),
383+
waitForUpdate: expect.any(Function),
373384
cancel: expect.any(Function),
374385
ok: expect.any(Function),
375386
});
@@ -1250,6 +1261,7 @@ describe('getInterface', () => {
12501261
selectFromRadioGroup: expect.any(Function),
12511262
selectFromSelector: expect.any(Function),
12521263
uploadFile: expect.any(Function),
1264+
waitForUpdate: expect.any(Function),
12531265
ok: expect.any(Function),
12541266
});
12551267
});
@@ -1280,6 +1292,7 @@ describe('getInterface', () => {
12801292
selectFromRadioGroup: expect.any(Function),
12811293
selectFromSelector: expect.any(Function),
12821294
uploadFile: expect.any(Function),
1295+
waitForUpdate: expect.any(Function),
12831296
ok: expect.any(Function),
12841297
});
12851298
});
@@ -1468,6 +1481,41 @@ describe('getInterface', () => {
14681481
},
14691482
);
14701483
});
1484+
1485+
it('waits for the interface content to update when `waitForUpdate` is called', async () => {
1486+
jest.spyOn(rootControllerMessenger, 'call');
1487+
const { store, runSaga } = createStore(getMockOptions());
1488+
1489+
const content = (
1490+
<Box>
1491+
<Input name="foo" />
1492+
</Box>
1493+
);
1494+
const id = await interfaceController.createInterface(MOCK_SNAP_ID, content);
1495+
const type = DialogType.Alert;
1496+
const ui = { type: DIALOG_APPROVAL_TYPES[type], id };
1497+
1498+
store.dispatch(setInterface(ui));
1499+
1500+
const result = await runSaga(
1501+
getInterface,
1502+
runSaga,
1503+
MOCK_SNAP_ID,
1504+
rootControllerMessenger,
1505+
).toPromise();
1506+
1507+
const promise = result.waitForUpdate();
1508+
1509+
await interfaceController.updateInterface(
1510+
MOCK_SNAP_ID,
1511+
id,
1512+
<Text>Hello world!</Text>,
1513+
);
1514+
1515+
const newInterface = await promise;
1516+
1517+
expect(newInterface.content.type).toBe('Text');
1518+
});
14711519
});
14721520

14731521
describe('selectFromRadioGroup', () => {
@@ -1761,3 +1809,40 @@ describe('selectFromSelector', () => {
17611809
);
17621810
});
17631811
});
1812+
1813+
describe('waitForUpdate', () => {
1814+
const rootControllerMessenger = getRootControllerMessenger();
1815+
const controllerMessenger = getRestrictedSnapInterfaceControllerMessenger(
1816+
rootControllerMessenger,
1817+
);
1818+
1819+
const interfaceController = new SnapInterfaceController({
1820+
messenger: controllerMessenger,
1821+
});
1822+
1823+
it('waits for the interface content to update', async () => {
1824+
const content = <Input name="foo" />;
1825+
1826+
const interfaceId = await interfaceController.createInterface(
1827+
MOCK_SNAP_ID,
1828+
content,
1829+
);
1830+
1831+
const promise = waitForUpdate(
1832+
rootControllerMessenger,
1833+
MOCK_SNAP_ID,
1834+
interfaceId,
1835+
content,
1836+
);
1837+
1838+
await interfaceController.updateInterface(
1839+
MOCK_SNAP_ID,
1840+
interfaceId,
1841+
<Text>Hello world!</Text>,
1842+
);
1843+
1844+
const newInterface = await promise;
1845+
1846+
expect(newInterface.content.type).toBe('Text');
1847+
});
1848+
});

packages/snaps-simulation/src/interface.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SnapInterfaceControllerState } from '@metamask/snaps-controllers';
12
import type { DialogApprovalTypes } from '@metamask/snaps-rpc-methods';
23
import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods';
34
import type {
@@ -19,14 +20,20 @@ import {
1920
} from '@metamask/snaps-utils';
2021
import { assertExhaustive, hasProperty } from '@metamask/utils';
2122
import type { PayloadAction } from '@reduxjs/toolkit';
23+
import deepEqual from 'fast-deep-equal';
2224
import { type SagaIterator } from 'redux-saga';
2325
import { call, put, select, take } from 'redux-saga/effects';
2426

2527
import type { RootControllerMessenger } from './controllers';
2628
import { getFileSize, getFileToUpload } from './files';
2729
import type { Interface, RunSagaFunction } from './store';
2830
import { getCurrentInterface, resolveInterface, setInterface } from './store';
29-
import type { FileOptions, SnapInterface, SnapInterfaceActions } from './types';
31+
import type {
32+
FileOptions,
33+
SnapHandlerInterface,
34+
SnapInterface,
35+
SnapInterfaceActions,
36+
} from './types';
3037

3138
/**
3239
* The maximum file size that can be uploaded.
@@ -752,6 +759,48 @@ export async function selectFromSelector(
752759
});
753760
}
754761

762+
/**
763+
* Wait for an interface to be updated.
764+
*
765+
* @param controllerMessenger - The controller messenger used to call actions.
766+
* @param snapId - The Snap ID.
767+
* @param id - The interface ID.
768+
* @param originalContent - The original interface content.
769+
* @returns A promise that resolves to the updated interface.
770+
*/
771+
export async function waitForUpdate(
772+
controllerMessenger: RootControllerMessenger,
773+
snapId: SnapId,
774+
id: string,
775+
originalContent: JSXElement,
776+
) {
777+
return new Promise<SnapHandlerInterface>((resolve) => {
778+
const listener = (state: SnapInterfaceControllerState) => {
779+
const currentInterface = state.interfaces[id];
780+
const newContent = currentInterface?.content;
781+
782+
if (!deepEqual(originalContent, newContent)) {
783+
controllerMessenger.unsubscribe(
784+
'SnapInterfaceController:stateChange',
785+
listener,
786+
);
787+
788+
const actions = getInterfaceActions(snapId, controllerMessenger, {
789+
content: newContent,
790+
id,
791+
});
792+
793+
resolve({ ...actions, content: newContent });
794+
}
795+
};
796+
797+
controllerMessenger.subscribe(
798+
'SnapInterfaceController:stateChange',
799+
listener,
800+
);
801+
});
802+
}
803+
755804
/**
756805
* Get a formatted file size.
757806
*
@@ -923,6 +972,9 @@ export function getInterfaceActions(
923972
options,
924973
);
925974
},
975+
976+
waitForUpdate: async () =>
977+
waitForUpdate(controllerMessenger, snapId, id, content),
926978
};
927979
}
928980

packages/snaps-simulation/src/request.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ describe('handleRequest', () => {
115115
selectInDropdown: expect.any(Function),
116116
typeInField: expect.any(Function),
117117
uploadFile: expect.any(Function),
118+
waitForUpdate: expect.any(Function),
118119
});
119120

120121
await closeServer();
@@ -346,6 +347,7 @@ describe('getInterfaceApi', () => {
346347
selectFromRadioGroup: expect.any(Function),
347348
selectFromSelector: expect.any(Function),
348349
uploadFile: expect.any(Function),
350+
waitForUpdate: expect.any(Function),
349351
});
350352
});
351353

@@ -379,6 +381,7 @@ describe('getInterfaceApi', () => {
379381
selectFromRadioGroup: expect.any(Function),
380382
selectFromSelector: expect.any(Function),
381383
uploadFile: expect.any(Function),
384+
waitForUpdate: expect.any(Function),
382385
});
383386
});
384387

packages/snaps-simulation/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ export type SnapInterfaceActions = {
173173
file: string | Uint8Array,
174174
options?: FileOptions,
175175
): Promise<void>;
176+
177+
/**
178+
* Wait for the interface to be updated.
179+
*/
180+
waitForUpdate: () => Promise<SnapHandlerInterface>;
176181
};
177182

178183
/**

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6145,6 +6145,7 @@ __metadata:
61456145
eslint-plugin-prettier: "npm:^4.2.1"
61466146
eslint-plugin-promise: "npm:^6.1.1"
61476147
express: "npm:^4.18.2"
6148+
fast-deep-equal: "npm:^3.1.3"
61486149
jest: "npm:^29.0.2"
61496150
jest-it-up: "npm:^2.0.0"
61506151
jest-silent-reporter: "npm:^0.6.0"

0 commit comments

Comments
 (0)