Skip to content

Commit 31bd332

Browse files
feat!: Ensure user has onboarded before allowing usage of SnapController (#3731)
Adds a new required constructor argument `ensureOnboardingComplete`. This should be a promise that resolves once the user has been onboarded. We can use this to ensure that a user has fully onboarded and decided whether to opt-out of Snaps before using Snaps at all. The function is used as part of `assertCanUsePlatform`. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Require onboarding completion via new `ensureOnboardingComplete` hook and asynchronously gate Snap operations (install, start, handle requests, registry updates) until onboarding finishes. > > - **SnapController API**: > - Add required constructor hook `ensureOnboardingComplete(): Promise<void>` and store internally. > - Make platform check async: `#assertCanUsePlatform` now awaits onboarding completion, then validates feature flags. > - Await platform check in `updateRegistry`, `startSnap`, `installSnaps`, `#updateSnap`, and `handleRequest`. > - **Tests**: > - Add test ensuring requests wait for onboarding completion before execution. > - Update helpers to pass mocked `ensureOnboardingComplete` by default. > - Adjust call-count expectations and minor timing in affected tests; remove a couple of unused delegated actions in test utils. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit dd19141. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent c7e59b6 commit 31bd332

File tree

3 files changed

+88
-23
lines changed

3 files changed

+88
-23
lines changed

packages/snaps-controllers/src/snaps/SnapController.test.tsx

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ describe('SnapController', () => {
795795
[MOCK_SNAP_ID]: expectedSnapObject,
796796
});
797797

798-
expect(messenger.call).toHaveBeenCalledTimes(10);
798+
expect(messenger.call).toHaveBeenCalledTimes(9);
799799

800800
expect(messenger.call).toHaveBeenNthCalledWith(
801801
1,
@@ -1869,10 +1869,12 @@ describe('SnapController', () => {
18691869
},
18701870
});
18711871

1872-
const results = await Promise.allSettled([
1873-
snapController.removeSnap(snap.id),
1874-
promise,
1875-
]);
1872+
const removeSnap = async () => {
1873+
await sleep(1);
1874+
return snapController.removeSnap(snap.id);
1875+
};
1876+
1877+
const results = await Promise.allSettled([removeSnap(), promise]);
18761878

18771879
expect(results[0].status).toBe('fulfilled');
18781880
expect(results[1].status).toBe('rejected');
@@ -2681,6 +2683,55 @@ describe('SnapController', () => {
26812683
},
26822684
);
26832685

2686+
it('ensures onboarding has completed before processing requests', async () => {
2687+
const rootMessenger = getControllerMessenger();
2688+
const messenger = getSnapControllerMessenger(rootMessenger);
2689+
2690+
const callActionSpy = jest.spyOn(messenger, 'call');
2691+
2692+
const { promise, resolve } = createDeferredPromise();
2693+
const ensureOnboardingComplete = jest.fn().mockReturnValue(promise);
2694+
const snapController = getSnapController(
2695+
getSnapControllerOptions({
2696+
messenger,
2697+
state: {
2698+
snaps: getPersistedSnapsState(),
2699+
},
2700+
ensureOnboardingComplete,
2701+
}),
2702+
);
2703+
2704+
const snap = snapController.getExpect(MOCK_SNAP_ID);
2705+
2706+
const requestPromise = snapController.handleRequest({
2707+
snapId: snap.id,
2708+
origin: METAMASK_ORIGIN,
2709+
handler: HandlerType.OnRpcRequest,
2710+
request: {
2711+
jsonrpc: '2.0',
2712+
method: 'test',
2713+
params: {},
2714+
},
2715+
});
2716+
2717+
await sleep(100);
2718+
2719+
expect(callActionSpy).not.toHaveBeenCalledWith(
2720+
'ExecutionService:executeSnap',
2721+
expect.objectContaining({ snapId: MOCK_SNAP_ID }),
2722+
);
2723+
2724+
resolve();
2725+
expect(await requestPromise).toBeUndefined();
2726+
2727+
expect(callActionSpy).toHaveBeenCalledWith(
2728+
'ExecutionService:executeSnap',
2729+
expect.objectContaining({ snapId: MOCK_SNAP_ID }),
2730+
);
2731+
2732+
snapController.destroy();
2733+
});
2734+
26842735
it('throws if the snap does not have permission to handle JSON-RPC requests from dapps', async () => {
26852736
const rootMessenger = getControllerMessenger();
26862737
const messenger = getSnapControllerMessenger(rootMessenger);
@@ -5448,7 +5499,7 @@ describe('SnapController', () => {
54485499

54495500
expect(result).toStrictEqual({ [MOCK_LOCAL_SNAP_ID]: truncatedSnap });
54505501

5451-
expect(messenger.call).toHaveBeenCalledTimes(12);
5502+
expect(messenger.call).toHaveBeenCalledTimes(11);
54525503

54535504
expect(messenger.call).toHaveBeenNthCalledWith(
54545505
1,
@@ -5598,7 +5649,7 @@ describe('SnapController', () => {
55985649
[MOCK_LOCAL_SNAP_ID]: truncatedSnap,
55995650
});
56005651

5601-
expect(messenger.call).toHaveBeenCalledTimes(23);
5652+
expect(messenger.call).toHaveBeenCalledTimes(22);
56025653

56035654
expect(messenger.call).toHaveBeenNthCalledWith(
56045655
1,
@@ -6845,7 +6896,7 @@ describe('SnapController', () => {
68456896
expect(result).toStrictEqual({
68466897
[MOCK_SNAP_ID]: truncatedSnap,
68476898
});
6848-
expect(messenger.call).toHaveBeenCalledTimes(10);
6899+
expect(messenger.call).toHaveBeenCalledTimes(9);
68496900

68506901
expect(messenger.call).toHaveBeenNthCalledWith(
68516902
1,
@@ -7538,7 +7589,7 @@ describe('SnapController', () => {
75387589
[MOCK_SNAP_ID]: { version: newVersionRange },
75397590
});
75407591

7541-
expect(messenger.call).toHaveBeenCalledTimes(21);
7592+
expect(messenger.call).toHaveBeenCalledTimes(20);
75427593

75437594
expect(messenger.call).toHaveBeenNthCalledWith(
75447595
3,
@@ -8414,7 +8465,7 @@ describe('SnapController', () => {
84148465
date: expect.any(Number),
84158466
},
84168467
]);
8417-
expect(callActionSpy).toHaveBeenCalledTimes(21);
8468+
expect(callActionSpy).toHaveBeenCalledTimes(20);
84188469

84198470
expect(callActionSpy).toHaveBeenNthCalledWith(
84208471
12,
@@ -8579,7 +8630,7 @@ describe('SnapController', () => {
85798630
[MOCK_SNAP_ID]: { version: '1.1.0' },
85808631
});
85818632

8582-
expect(callActionSpy).toHaveBeenCalledTimes(21);
8633+
expect(callActionSpy).toHaveBeenCalledTimes(20);
85838634
expect(callActionSpy).toHaveBeenNthCalledWith(
85848635
12,
85858636
'ApprovalController:addRequest',
@@ -8695,7 +8746,7 @@ describe('SnapController', () => {
86958746
[MOCK_SNAP_ID]: { version: '1.1.0' },
86968747
});
86978748

8698-
expect(callActionSpy).toHaveBeenCalledTimes(22);
8749+
expect(callActionSpy).toHaveBeenCalledTimes(21);
86998750
expect(callActionSpy).toHaveBeenNthCalledWith(
87008751
12,
87018752
'ApprovalController:addRequest',
@@ -8836,7 +8887,7 @@ describe('SnapController', () => {
88368887

88378888
const isRunning = controller.isRunning(MOCK_SNAP_ID);
88388889

8839-
expect(callActionSpy).toHaveBeenCalledTimes(12);
8890+
expect(callActionSpy).toHaveBeenCalledTimes(11);
88408891

88418892
expect(callActionSpy).toHaveBeenNthCalledWith(
88428893
1,
@@ -9191,7 +9242,7 @@ describe('SnapController', () => {
91919242
[MOCK_SNAP_ID]: { version: '1.1.0' },
91929243
});
91939244

9194-
expect(callActionSpy).toHaveBeenCalledTimes(23);
9245+
expect(callActionSpy).toHaveBeenCalledTimes(22);
91959246

91969247
expect(callActionSpy).toHaveBeenNthCalledWith(
91979248
12,

packages/snaps-controllers/src/snaps/SnapController.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,13 @@ type SnapControllerArgs = {
815815
* MetaMetrics event tracking hook.
816816
*/
817817
trackEvent: TrackEventHook;
818+
819+
/**
820+
* A hook that returns a promise that resolves when the onboarding has completed.
821+
*
822+
* @returns A promise that resolves when onboarding is complete.
823+
*/
824+
ensureOnboardingComplete: () => Promise<void>;
818825
};
819826

820827
type AddSnapArgs = {
@@ -932,6 +939,8 @@ export class SnapController extends BaseController<
932939

933940
readonly #trackSnapExport: ReturnType<typeof throttleTracking>;
934941

942+
readonly #ensureOnboardingComplete: () => Promise<void>;
943+
935944
constructor({
936945
closeAllConnections,
937946
messenger,
@@ -951,6 +960,7 @@ export class SnapController extends BaseController<
951960
getFeatureFlags = () => ({}),
952961
clientCryptography,
953962
trackEvent,
963+
ensureOnboardingComplete,
954964
}: SnapControllerArgs) {
955965
super({
956966
messenger,
@@ -1034,6 +1044,7 @@ export class SnapController extends BaseController<
10341044
this.#rollbackSnapshots = new Map();
10351045
this.#snapsRuntimeData = new Map();
10361046
this.#trackEvent = trackEvent;
1047+
this.#ensureOnboardingComplete = ensureOnboardingComplete;
10371048

10381049
this.#pollForLastRequestStatus();
10391050

@@ -1467,7 +1478,7 @@ export class SnapController extends BaseController<
14671478
* Also updates any preinstalled Snaps to the latest allowlisted version.
14681479
*/
14691480
async updateRegistry(): Promise<void> {
1470-
this.#assertCanUsePlatform();
1481+
await this.#assertCanUsePlatform();
14711482
await this.messenger.call('SnapsRegistry:update');
14721483

14731484
const blockedSnaps = await this.messenger.call(
@@ -1645,9 +1656,12 @@ export class SnapController extends BaseController<
16451656
}
16461657

16471658
/**
1648-
* Asserts whether the Snaps platform is allowed to run.
1659+
* Waits for onboarding and then asserts whether the Snaps platform is allowed to run.
16491660
*/
1650-
#assertCanUsePlatform() {
1661+
async #assertCanUsePlatform() {
1662+
// Ensure the user has onboarded before allowing access to Snaps.
1663+
await this.#ensureOnboardingComplete();
1664+
16511665
const flags = this.#getFeatureFlags();
16521666
assert(
16531667
flags.disableSnaps !== true,
@@ -1730,7 +1744,7 @@ export class SnapController extends BaseController<
17301744
* @param snapId - The id of the Snap to start.
17311745
*/
17321746
async startSnap(snapId: SnapId): Promise<void> {
1733-
this.#assertCanUsePlatform();
1747+
await this.#assertCanUsePlatform();
17341748
const snap = this.state.snaps[snapId];
17351749

17361750
if (!snap.enabled) {
@@ -2630,7 +2644,7 @@ export class SnapController extends BaseController<
26302644
origin: string,
26312645
requestedSnaps: RequestSnapsParams,
26322646
): Promise<RequestSnapsResult> {
2633-
this.#assertCanUsePlatform();
2647+
await this.#assertCanUsePlatform();
26342648

26352649
const result: RequestSnapsResult = {};
26362650

@@ -2916,7 +2930,7 @@ export class SnapController extends BaseController<
29162930
if (!automaticUpdate) {
29172931
this.#assertCanInstallSnaps();
29182932
}
2919-
this.#assertCanUsePlatform();
2933+
await this.#assertCanUsePlatform();
29202934

29212935
const snap = this.getExpect(snapId);
29222936

@@ -3551,7 +3565,7 @@ export class SnapController extends BaseController<
35513565
handler: handlerType,
35523566
request: rawRequest,
35533567
}: SnapRpcHookArgs & { snapId: SnapId }): Promise<unknown> {
3554-
this.#assertCanUsePlatform();
3568+
await this.#assertCanUsePlatform();
35553569

35563570
const snap = this.get(snapId);
35573571

packages/snaps-controllers/src/test-utils/controller.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,6 @@ export const getSnapControllerMessenger = (
479479
'ExecutionService:executeSnap',
480480
'ExecutionService:terminateSnap',
481481
'ExecutionService:handleRpcRequest',
482-
'NetworkController:getNetworkClientById',
483482
'PermissionController:getEndowments',
484483
'PermissionController:hasPermission',
485484
'PermissionController:hasPermissions',
@@ -490,7 +489,6 @@ export const getSnapControllerMessenger = (
490489
'PermissionController:revokePermissionForAllSubjects',
491490
'PermissionController:updateCaveat',
492491
'PermissionController:getSubjectNames',
493-
'SelectedNetworkController:getNetworkClientIdForDomain',
494492
'SubjectMetadataController:getSubjectMetadata',
495493
'SubjectMetadataController:addSubjectMetadata',
496494
'SnapsRegistry:get',
@@ -574,6 +572,7 @@ export const getSnapControllerOptions = (
574572
clientCryptography: {},
575573
encryptor: getSnapControllerEncryptor(),
576574
trackEvent: jest.fn(),
575+
ensureOnboardingComplete: jest.fn().mockResolvedValue(undefined),
577576
...opts,
578577
} as SnapControllerConstructorParams;
579578

@@ -608,6 +607,7 @@ export const getSnapControllerWithEESOptions = ({
608607
encryptor: getSnapControllerEncryptor(),
609608
fetchFunction: jest.fn(),
610609
trackEvent: jest.fn(),
610+
ensureOnboardingComplete: jest.fn().mockResolvedValue(undefined),
611611
...options,
612612
} as SnapControllerConstructorParams & {
613613
rootMessenger: ReturnType<typeof getControllerMessenger>;

0 commit comments

Comments
 (0)