diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index a23085242b..ddabf00d96 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -10624,6 +10624,8 @@ describe('SnapController', () => { await waitForStateChange(messenger); + await waitForStateChange(messenger); + expect(snapController.isRunning(MOCK_SNAP_ID)).toBe(true); await new Promise((resolve) => setTimeout(resolve, 100)); @@ -10647,6 +10649,7 @@ describe('SnapController', () => { ); expect(snapController.state).toStrictEqual({ + isReady: false, snaps: {}, snapStates: {}, unencryptedSnapStates: {}, @@ -10769,6 +10772,23 @@ describe('SnapController', () => { describe('SnapController actions', () => { describe('SnapController:init', () => { + it('populates `isReady`', async () => { + const rootMessenger = getControllerMessenger(); + const messenger = getSnapControllerMessenger(rootMessenger); + + const snapController = getSnapController( + getSnapControllerOptions({ messenger }), + ); + + expect(snapController.state.isReady).toBe(false); + messenger.call('SnapController:init'); + + await waitForStateChange(messenger); + expect(snapController.state.isReady).toBe(true); + + snapController.destroy(); + }); + it('calls `onStart` for all Snaps with the `endowment:lifecycle-hooks` permission', async () => { const rootMessenger = getControllerMessenger(); const messenger = getSnapControllerMessenger(rootMessenger); @@ -12997,7 +13017,11 @@ describe('SnapController', () => { controller.metadata, 'includeInDebugSnapshot', ), - ).toMatchInlineSnapshot(`{}`); + ).toMatchInlineSnapshot(` + { + "isReady": false, + } + `); }); describe('includeInStateLogs', () => { @@ -13012,6 +13036,7 @@ describe('SnapController', () => { ), ).toMatchInlineSnapshot(` { + "isReady": false, "snaps": {}, } `); diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 7b32f7a754..eb16034a41 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -318,6 +318,7 @@ export type SnapControllerState = { snaps: StoredSnaps; snapStates: Record; unencryptedSnapStates: Record; + isReady: boolean; }; export type PersistedSnapControllerState = SnapControllerState & { @@ -854,6 +855,7 @@ const defaultState: SnapControllerState = { snaps: {}, snapStates: {}, unencryptedSnapStates: {}, + isReady: false, }; /** @@ -965,6 +967,12 @@ export class SnapController extends BaseController< super({ messenger, metadata: { + isReady: { + includeInStateLogs: true, + includeInDebugSnapshot: true, + persist: false, + usedInUi: false, + }, snapStates: { includeInStateLogs: false, persist: true, @@ -1327,6 +1335,9 @@ export class SnapController extends BaseController< * runnable Snaps. */ init() { + // Lazily populate the `isReady` state. + this.#ensureCanUsePlatform().catch(logWarning); + this.#callLifecycleHooks(METAMASK_ORIGIN, HandlerType.OnStart); } @@ -1537,7 +1548,7 @@ export class SnapController extends BaseController< * Also updates any preinstalled Snaps to the latest allowlisted version. */ async updateRegistry(): Promise { - await this.#assertCanUsePlatform(); + await this.#ensureCanUsePlatform(); await this.messenger.call('SnapsRegistry:update'); const blockedSnaps = await this.messenger.call( @@ -1717,13 +1728,23 @@ export class SnapController extends BaseController< /** * Waits for onboarding and then asserts whether the Snaps platform is allowed to run. */ - async #assertCanUsePlatform() { + async #ensureCanUsePlatform() { // Ensure the user has onboarded before allowing access to Snaps. await this.#ensureOnboardingComplete(); const flags = this.#getFeatureFlags(); + + // If the user has onboarded, the Snaps Platform is considered ready, + // if it isn't forced to be disabled via feature flags. + const isReady = flags.disableSnaps !== true; + if (this.state.isReady !== isReady) { + this.update((state) => { + state.isReady = isReady; + }); + } + assert( - flags.disableSnaps !== true, + isReady, 'The Snaps platform requires basic functionality to be used. Enable basic functionality in the settings to use the Snaps platform.', ); } @@ -1803,7 +1824,7 @@ export class SnapController extends BaseController< * @param snapId - The id of the Snap to start. */ async startSnap(snapId: SnapId): Promise { - await this.#assertCanUsePlatform(); + await this.#ensureCanUsePlatform(); const snap = this.state.snaps[snapId]; if (!snap.enabled) { @@ -2387,6 +2408,7 @@ export class SnapController extends BaseController< snapIds.forEach((snapId) => this.#revokeAllSnapPermissions(snapId)); this.update((state) => { + state.isReady = false; state.snaps = {}; state.snapStates = {}; state.unencryptedSnapStates = {}; @@ -2703,7 +2725,7 @@ export class SnapController extends BaseController< origin: string, requestedSnaps: RequestSnapsParams, ): Promise { - await this.#assertCanUsePlatform(); + await this.#ensureCanUsePlatform(); const result: RequestSnapsResult = {}; @@ -2989,7 +3011,7 @@ export class SnapController extends BaseController< if (!automaticUpdate) { this.#assertCanInstallSnaps(); } - await this.#assertCanUsePlatform(); + await this.#ensureCanUsePlatform(); const snap = this.getExpect(snapId); @@ -3624,7 +3646,7 @@ export class SnapController extends BaseController< handler: handlerType, request: rawRequest, }: SnapRpcHookArgs & { snapId: SnapId }): Promise { - await this.#assertCanUsePlatform(); + await this.#ensureCanUsePlatform(); const snap = this.get(snapId);