From 7f5ea5326d0ebe3af5c9c0c6643a9dc55622bf4b Mon Sep 17 00:00:00 2001 From: david0xd Date: Thu, 5 Mar 2026 17:14:59 +0100 Subject: [PATCH 01/11] Fix Snap Platform Watcher throwing error after wallet reset --- .../src/snaps/SnapPlatformWatcher.test.ts | 50 ++++++++++++++----- .../src/snaps/SnapPlatformWatcher.ts | 10 +++- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index 39f4030e541..d13caae7344 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -90,20 +90,31 @@ describe('SnapPlatformWatcher', () => { expect(resolved).toBe(true); }); - it('throws error if platform becomes unavailable after being ready once', async () => { + it('waits for platform to be ready again when it becomes unavailable (e.g. after wallet reset)', async () => { const { rootMessenger, messenger } = setup(); const watcher = new SnapPlatformWatcher(messenger); // Make platform ready first. publishIsReadyState(rootMessenger, true); - // Make platform unavailable + // Make platform unavailable (e.g. clearState during wallet reset). publishIsReadyState(rootMessenger, false); - // Should throw error since platform is not ready now. - await expect(watcher.ensureCanUseSnapPlatform()).rejects.toThrow( - 'Snap platform cannot be used now.', - ); + // ensureCanUseSnapPlatform() should wait for the next ready, not throw. + const ensurePromise = watcher.ensureCanUseSnapPlatform(); + let resolved = false; + void ensurePromise.then(() => { + resolved = true; + return null; + }); + + expect(resolved).toBe(false); + + // Platform becomes ready again (e.g. Snap controller re-initialized). + publishIsReadyState(rootMessenger, true); + + await ensurePromise; + expect(resolved).toBe(true); }); it('handles multiple state changes correctly', async () => { @@ -119,13 +130,10 @@ describe('SnapPlatformWatcher', () => { // Make platform unavailable. publishIsReadyState(rootMessenger, false); - // Should fail. - await expect(watcher.ensureCanUseSnapPlatform()).rejects.toThrow( - 'Snap platform cannot be used now.', - ); - - // Make platform ready again. + // ensureCanUseSnapPlatform() now waits for the next ready (does not throw). + const ensurePromise = watcher.ensureCanUseSnapPlatform(); publishIsReadyState(rootMessenger, true); + await ensurePromise; // Should work again. expect(await watcher.ensureCanUseSnapPlatform()).toBeUndefined(); @@ -192,6 +200,24 @@ describe('SnapPlatformWatcher', () => { expect(resolved).toBe(true); }); + it('throws if platform becomes not ready again before the await continuation runs (race guard)', async () => { + const { rootMessenger, messenger } = setup(); + const watcher = new SnapPlatformWatcher(messenger); + + // Start waiting for the platform. + const ensurePromise = watcher.ensureCanUseSnapPlatform(); + + // Make platform ready (resolves the deferred; continuation is queued as microtask). + publishIsReadyState(rootMessenger, true); + // Before the continuation runs, make platform not ready again. + publishIsReadyState(rootMessenger, false); + + // The continuation runs after both publishes; it sees isReady false and throws. + await expect(ensurePromise).rejects.toThrow( + 'Snap platform cannot be used now.', + ); + }); + it('resolves immediately if platform is already ready', async () => { const { messenger, mocks } = setup(); diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index 8e0f2304775..b8cd0934c97 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -7,7 +7,7 @@ import { MultichainAccountServiceMessenger } from '../types'; export class SnapPlatformWatcher { readonly #messenger: MultichainAccountServiceMessenger; - readonly #isReadyOnce: DeferredPromise; + #isReadyOnce: DeferredPromise; #isReady: boolean; @@ -44,15 +44,21 @@ export class SnapPlatformWatcher { this.#isReadyOnce.resolve(); } - // We still subscribe to state changes to keep track of the platform's readiness. + // We must always subscribe to state changes to keep track of the platform's readiness. this.#messenger.subscribe( 'SnapController:stateChange', (isReady: boolean) => { + const wasReady = this.#isReady; this.#isReady = isReady; if (isReady) { logReadyOnce(); this.#isReadyOnce.resolve(); + } else if (wasReady) { + // Platform was ready, and now it is not. + // ensureCanUseSnapPlatform() will wait again. + // This is required after the wallet reset. + this.#isReadyOnce = createDeferredPromise(); } }, (state) => state.isReady, From 51e041c2d795fa63d7a77f5c568c19d5f4e24612 Mon Sep 17 00:00:00 2001 From: david0xd Date: Thu, 5 Mar 2026 17:19:45 +0100 Subject: [PATCH 02/11] Add changelog entry --- packages/accounts-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index d522595c14d..213b142423a 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- SnapPlatformWatcher now waits for the Snap platform to be ready again after wallet reset, fixing "Snap platform cannot be used now" when creating Snap-dependent accounts. + ### Added - Expose missing public `AccountsController` methods through its messenger ([#7976](https://github.com/MetaMask/core/pull/7976/)) From 0129ecc405330f33482ac229175d0d70d4cb0ec0 Mon Sep 17 00:00:00 2001 From: david0xd Date: Thu, 5 Mar 2026 17:22:51 +0100 Subject: [PATCH 03/11] Update Changelog entry with PR number and link --- packages/accounts-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index 213b142423a..8dfeca70e7a 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- SnapPlatformWatcher now waits for the Snap platform to be ready again after wallet reset, fixing "Snap platform cannot be used now" when creating Snap-dependent accounts. +- SnapPlatformWatcher now waits for the Snap platform to be ready again after wallet reset, fixing "Snap platform cannot be used now" when creating Snap-dependent accounts ([#8124](https://github.com/MetaMask/core/pull/8124)) ### Added From 7fc4a693007828b3811295ece6b0757985d98dd4 Mon Sep 17 00:00:00 2001 From: david0xd Date: Thu, 5 Mar 2026 17:33:27 +0100 Subject: [PATCH 04/11] Update changelog order --- packages/accounts-controller/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index 8dfeca70e7a..edbf3e8f80f 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -7,10 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed - -- SnapPlatformWatcher now waits for the Snap platform to be ready again after wallet reset, fixing "Snap platform cannot be used now" when creating Snap-dependent accounts ([#8124](https://github.com/MetaMask/core/pull/8124)) - ### Added - Expose missing public `AccountsController` methods through its messenger ([#7976](https://github.com/MetaMask/core/pull/7976/)) @@ -18,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `AccountController:loadBackupAction` - Corresponding action types (e.g. `AccountControllerLoadBackupAction`) are available as well. +### Fixed + +- SnapPlatformWatcher now waits for the Snap platform to be ready again after wallet reset, fixing "Snap platform cannot be used now" when creating Snap-dependent accounts ([#8124](https://github.com/MetaMask/core/pull/8124)) + ### Deprecated - Mark `AccountsController`, all of its public methods, and all exported types as deprecated in favor of `AccountTreeController`, `MultichainAccountService`, and Keyring API v2 ([#8027](https://github.com/MetaMask/core/pull/8027)) From ef1420a1b116c8b2688c407f6a1803f1d0ad047d Mon Sep 17 00:00:00 2001 From: david0xd Date: Thu, 5 Mar 2026 17:34:35 +0100 Subject: [PATCH 05/11] Update changelog entry description --- packages/accounts-controller/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index edbf3e8f80f..9424bf84d4b 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -16,7 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- SnapPlatformWatcher now waits for the Snap platform to be ready again after wallet reset, fixing "Snap platform cannot be used now" when creating Snap-dependent accounts ([#8124](https://github.com/MetaMask/core/pull/8124)) +- `SnapPlatformWatcher` now waits for the Snap platform to be ready again after wallet reset ([#8124](https://github.com/MetaMask/core/pull/8124)) + - Fixing "Snap platform cannot be used now." when creating Snap-dependent accounts. ### Deprecated From 16a3760a8b3839ba2bf35d9e21fdbce5821f70ca Mon Sep 17 00:00:00 2001 From: david0xd Date: Fri, 6 Mar 2026 16:18:22 +0100 Subject: [PATCH 06/11] Change approach for Snap Platform Watcher (add onboarding dependency) --- packages/accounts-controller/CHANGELOG.md | 5 -- .../multichain-account-service/CHANGELOG.md | 6 ++ .../src/MultichainAccountService.ts | 11 ++- .../src/snaps/SnapPlatformWatcher.test.ts | 71 +++++++++++-------- .../src/snaps/SnapPlatformWatcher.ts | 33 +++++---- 5 files changed, 78 insertions(+), 48 deletions(-) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index 9424bf84d4b..d522595c14d 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -14,11 +14,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `AccountController:loadBackupAction` - Corresponding action types (e.g. `AccountControllerLoadBackupAction`) are available as well. -### Fixed - -- `SnapPlatformWatcher` now waits for the Snap platform to be ready again after wallet reset ([#8124](https://github.com/MetaMask/core/pull/8124)) - - Fixing "Snap platform cannot be used now." when creating Snap-dependent accounts. - ### Deprecated - Mark `AccountsController`, all of its public methods, and all exported types as deprecated in favor of `AccountTreeController`, `MultichainAccountService`, and Keyring API v2 ([#8027](https://github.com/MetaMask/core/pull/8027)) diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index 2c5ceee4713..84a8572c7c5 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -9,8 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **SnapPlatformWatcher** accepts optional `ensureOnboardingComplete` in options ([#8124](https://github.com/MetaMask/core/pull/8124)) - Bump `@metamask/accounts-controller` from `^36.0.0` to `^36.0.1` ([#7996](https://github.com/MetaMask/core/pull/7996)) +### Fixed + +- `SnapPlatformWatcher` now waits for the Snap platform to be ready again after wallet reset ([#8124](https://github.com/MetaMask/core/pull/8124)) + - Fixing "Snap platform cannot be used now." by additionally waiting for the onboarding to be completed. + ## [7.0.0] ### Changed diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts index 792c1f9b451..623f4cf21e2 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -51,6 +51,10 @@ export type MultichainAccountServiceOptions = { [SOL_ACCOUNT_PROVIDER_NAME]?: SolAccountProviderConfig; }; config?: MultichainAccountServiceConfig; + /** + * When provided, used to prevent using Snap platform before onboarding completion. + */ + ensureOnboardingComplete?: () => Promise; }; /** @@ -135,12 +139,15 @@ export class MultichainAccountService { * @param options.providers - Optional list of account * @param options.providerConfigs - Optional provider configs * @param options.config - Optional config. + * @param options.ensureOnboardingComplete - Optional callback to ensure + * onboarding is completed before using the Snap platform. */ constructor({ messenger, providers = [], providerConfigs, config, + ensureOnboardingComplete, }: MultichainAccountServiceOptions) { this.#messenger = messenger; this.#wallets = new Map(); @@ -168,7 +175,9 @@ export class MultichainAccountService { ...providers, ]; - this.#watcher = new SnapPlatformWatcher(messenger); + this.#watcher = new SnapPlatformWatcher(messenger, { + ensureOnboardingComplete, + }); this.#messenger.registerMethodActionHandlers( this, diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index d13caae7344..9b9f2bd3d46 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -57,13 +57,24 @@ function publishIsReadyState(messenger: RootMessenger, isReady: boolean): void { describe('SnapPlatformWatcher', () => { describe('constructor', () => { - it('initializes with isReady as false', () => { + it('initializes with isReady as false when not using ensureOnboardingComplete', () => { const { messenger } = setup(); const watcher = new SnapPlatformWatcher(messenger); expect(watcher).toBeDefined(); expect(watcher.isReady).toBe(false); }); + + it('still tracks Snap platform state when using ensureOnboardingComplete', () => { + const { messenger } = setup(); + const watcher = new SnapPlatformWatcher(messenger, { + ensureOnboardingComplete: (): Promise => Promise.resolve(), + }); + + expect(watcher).toBeDefined(); + // isReady reflects SnapController state, not the callback (both are required). + expect(watcher.isReady).toBe(false); + }); }); describe('ensureCanUsePlatform', () => { @@ -90,52 +101,31 @@ describe('SnapPlatformWatcher', () => { expect(resolved).toBe(true); }); - it('waits for platform to be ready again when it becomes unavailable (e.g. after wallet reset)', async () => { + it('throws error if platform becomes unavailable after being ready once (SnapController fallback)', async () => { const { rootMessenger, messenger } = setup(); const watcher = new SnapPlatformWatcher(messenger); - // Make platform ready first. publishIsReadyState(rootMessenger, true); - - // Make platform unavailable (e.g. clearState during wallet reset). publishIsReadyState(rootMessenger, false); - // ensureCanUseSnapPlatform() should wait for the next ready, not throw. - const ensurePromise = watcher.ensureCanUseSnapPlatform(); - let resolved = false; - void ensurePromise.then(() => { - resolved = true; - return null; - }); - - expect(resolved).toBe(false); - - // Platform becomes ready again (e.g. Snap controller re-initialized). - publishIsReadyState(rootMessenger, true); - - await ensurePromise; - expect(resolved).toBe(true); + await expect(watcher.ensureCanUseSnapPlatform()).rejects.toThrow( + 'Snap platform cannot be used now.', + ); }); it('handles multiple state changes correctly', async () => { const { rootMessenger, messenger } = setup(); const watcher = new SnapPlatformWatcher(messenger); - // Make platform ready publishIsReadyState(rootMessenger, true); - - // Should work expect(await watcher.ensureCanUseSnapPlatform()).toBeUndefined(); - // Make platform unavailable. publishIsReadyState(rootMessenger, false); + await expect(watcher.ensureCanUseSnapPlatform()).rejects.toThrow( + 'Snap platform cannot be used now.', + ); - // ensureCanUseSnapPlatform() now waits for the next ready (does not throw). - const ensurePromise = watcher.ensureCanUseSnapPlatform(); publishIsReadyState(rootMessenger, true); - await ensurePromise; - - // Should work again. expect(await watcher.ensureCanUseSnapPlatform()).toBeUndefined(); }); @@ -221,7 +211,6 @@ describe('SnapPlatformWatcher', () => { it('resolves immediately if platform is already ready', async () => { const { messenger, mocks } = setup(); - // Make the platform ready before creating the watcher. mocks.SnapController.getState.mockReturnValue({ isReady: true, } as SnapControllerState); @@ -230,5 +219,27 @@ describe('SnapPlatformWatcher', () => { expect(watcher.isReady).toBe(true); }); + + it('requires both onboarding complete and Snap platform ready when ensureOnboardingComplete is provided', async () => { + const { rootMessenger, messenger } = setup(); + const ensureOnboardingComplete = jest.fn().mockResolvedValue(undefined); + const watcher = new SnapPlatformWatcher(messenger, { + ensureOnboardingComplete, + }); + + const ensurePromise = watcher.ensureCanUseSnapPlatform(); + let resolved = false; + void ensurePromise.then(() => { + resolved = true; + return null; + }); + + expect(ensureOnboardingComplete).toHaveBeenCalledTimes(1); + expect(resolved).toBe(false); + + publishIsReadyState(rootMessenger, true); + await ensurePromise; + expect(resolved).toBe(true); + }); }); }); diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index b8cd0934c97..20f27f71823 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -4,15 +4,28 @@ import { once } from 'lodash'; import { projectLogger as log } from '../logger'; import { MultichainAccountServiceMessenger } from '../types'; +export type SnapPlatformWatcherOptions = { + /** + * Resolves when onboarding is complete. + */ + ensureOnboardingComplete?: () => Promise; +}; + export class SnapPlatformWatcher { readonly #messenger: MultichainAccountServiceMessenger; - #isReadyOnce: DeferredPromise; + readonly #ensureOnboardingComplete?: () => Promise; + + readonly #isReadyOnce: DeferredPromise; #isReady: boolean; - constructor(messenger: MultichainAccountServiceMessenger) { + constructor( + messenger: MultichainAccountServiceMessenger, + options: SnapPlatformWatcherOptions = {}, + ) { this.#messenger = messenger; + this.#ensureOnboardingComplete = options.ensureOnboardingComplete; this.#isReady = false; this.#isReadyOnce = createDeferredPromise(); @@ -25,10 +38,14 @@ export class SnapPlatformWatcher { } async ensureCanUseSnapPlatform(): Promise { - // We always wait for the Snap platform to be ready at least once. + // When ensureOnboardingComplete is provided, wait for the onboarding first. + if (this.#ensureOnboardingComplete !== undefined) { + await this.#ensureOnboardingComplete(); + } + + // In all cases, we also require the Snap platform to be ready and available. await this.#isReadyOnce.promise; - // Then, we check for the current state and see if we can use it. if (!this.#isReady) { throw new Error('Snap platform cannot be used now.'); } @@ -37,28 +54,20 @@ export class SnapPlatformWatcher { #watch(): void { const logReadyOnce = once(() => log('Snap platform is ready!')); - // If already ready, resolve immediately. const initialState = this.#messenger.call('SnapController:getState'); if (initialState.isReady) { this.#isReady = true; this.#isReadyOnce.resolve(); } - // We must always subscribe to state changes to keep track of the platform's readiness. this.#messenger.subscribe( 'SnapController:stateChange', (isReady: boolean) => { - const wasReady = this.#isReady; this.#isReady = isReady; if (isReady) { logReadyOnce(); this.#isReadyOnce.resolve(); - } else if (wasReady) { - // Platform was ready, and now it is not. - // ensureCanUseSnapPlatform() will wait again. - // This is required after the wallet reset. - this.#isReadyOnce = createDeferredPromise(); } }, (state) => state.isReady, From 4b6dcdebf517a1404e8cef5ff9786cc7410ad64a Mon Sep 17 00:00:00 2001 From: david0xd Date: Fri, 6 Mar 2026 16:19:55 +0100 Subject: [PATCH 07/11] Revert some test changes --- .../src/snaps/SnapPlatformWatcher.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index 9b9f2bd3d46..8b0ae5888cc 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -101,13 +101,17 @@ describe('SnapPlatformWatcher', () => { expect(resolved).toBe(true); }); - it('throws error if platform becomes unavailable after being ready once (SnapController fallback)', async () => { + it('throws error if platform becomes unavailable after being ready once', async () => { const { rootMessenger, messenger } = setup(); const watcher = new SnapPlatformWatcher(messenger); + // Make platform ready first. publishIsReadyState(rootMessenger, true); + + // Make platform unavailable publishIsReadyState(rootMessenger, false); + // Should throw error since platform is not ready now. await expect(watcher.ensureCanUseSnapPlatform()).rejects.toThrow( 'Snap platform cannot be used now.', ); From 147ba9f463776db6dcd3e904ea4b5fe769db7aee Mon Sep 17 00:00:00 2001 From: david0xd Date: Fri, 6 Mar 2026 16:22:26 +0100 Subject: [PATCH 08/11] Revert some test changes (comments) --- .../src/snaps/SnapPlatformWatcher.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index 8b0ae5888cc..81cb5f5fc3d 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -121,15 +121,24 @@ describe('SnapPlatformWatcher', () => { const { rootMessenger, messenger } = setup(); const watcher = new SnapPlatformWatcher(messenger); + // Make platform ready publishIsReadyState(rootMessenger, true); + + // Should work expect(await watcher.ensureCanUseSnapPlatform()).toBeUndefined(); + // Make platform unavailable. publishIsReadyState(rootMessenger, false); + + // Should fail. await expect(watcher.ensureCanUseSnapPlatform()).rejects.toThrow( 'Snap platform cannot be used now.', ); + // Make platform ready again. publishIsReadyState(rootMessenger, true); + + // Should work again. expect(await watcher.ensureCanUseSnapPlatform()).toBeUndefined(); }); @@ -215,6 +224,7 @@ describe('SnapPlatformWatcher', () => { it('resolves immediately if platform is already ready', async () => { const { messenger, mocks } = setup(); + // Make the platform ready before creating the watcher. mocks.SnapController.getState.mockReturnValue({ isReady: true, } as SnapControllerState); From 6af62e154be0428bbacf8ef48beba97099cc6f1c Mon Sep 17 00:00:00 2001 From: david0xd Date: Fri, 6 Mar 2026 17:00:57 +0100 Subject: [PATCH 09/11] Update changelog --- packages/multichain-account-service/CHANGELOG.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index 84a8572c7c5..b21677c0a3c 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -7,15 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Changed +### Added -- **SnapPlatformWatcher** accepts optional `ensureOnboardingComplete` in options ([#8124](https://github.com/MetaMask/core/pull/8124)) -- Bump `@metamask/accounts-controller` from `^36.0.0` to `^36.0.1` ([#7996](https://github.com/MetaMask/core/pull/7996)) +- Add new optional `ensureOnboardingComplete` callback ([#8124](https://github.com/MetaMask/core/pull/8124)) + - This allows the service to wait for the user to re-onboard after a wallet reset. -### Fixed +### Changed -- `SnapPlatformWatcher` now waits for the Snap platform to be ready again after wallet reset ([#8124](https://github.com/MetaMask/core/pull/8124)) - - Fixing "Snap platform cannot be used now." by additionally waiting for the onboarding to be completed. +- Bump `@metamask/accounts-controller` from `^36.0.0` to `^36.0.1` ([#7996](https://github.com/MetaMask/core/pull/7996)) ## [7.0.0] From 3e64aa8472290599f0d097383189f2616eeac5e9 Mon Sep 17 00:00:00 2001 From: david0xd Date: Fri, 6 Mar 2026 17:02:45 +0100 Subject: [PATCH 10/11] Update Snap Watcher await for onboarding completion --- .../src/snaps/SnapPlatformWatcher.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index 20f27f71823..baea7452eb2 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -39,9 +39,7 @@ export class SnapPlatformWatcher { async ensureCanUseSnapPlatform(): Promise { // When ensureOnboardingComplete is provided, wait for the onboarding first. - if (this.#ensureOnboardingComplete !== undefined) { - await this.#ensureOnboardingComplete(); - } + await this.#ensureOnboardingComplete?.(); // In all cases, we also require the Snap platform to be ready and available. await this.#isReadyOnce.promise; From 12b2b5702a3c55284b6e75f5320f4121886b3ada Mon Sep 17 00:00:00 2001 From: david0xd Date: Fri, 6 Mar 2026 17:18:24 +0100 Subject: [PATCH 11/11] Add unit test --- .../src/snaps/SnapPlatformWatcher.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index 81cb5f5fc3d..2c01d5a8f84 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -1,5 +1,6 @@ /* eslint-disable no-void */ import { SnapControllerState } from '@metamask/snaps-controllers'; +import { createDeferredPromise } from '@metamask/utils'; import { SnapPlatformWatcher } from './SnapPlatformWatcher'; import { @@ -234,6 +235,34 @@ describe('SnapPlatformWatcher', () => { expect(watcher.isReady).toBe(true); }); + it('waits for ensureOnboardingComplete first when platform is already ready', async () => { + const { rootMessenger, messenger } = setup(); + const { promise: onboardingPromise, resolve: resolveOnboarding } = + createDeferredPromise(); + const ensureOnboardingComplete = jest + .fn() + .mockReturnValue(onboardingPromise); + const watcher = new SnapPlatformWatcher(messenger, { + ensureOnboardingComplete, + }); + + publishIsReadyState(rootMessenger, true); + + const ensurePromise = watcher.ensureCanUseSnapPlatform(); + let resolved = false; + void ensurePromise.then(() => { + resolved = true; + return null; + }); + + expect(ensureOnboardingComplete).toHaveBeenCalledTimes(1); + expect(resolved).toBe(false); + + resolveOnboarding(); + await ensurePromise; + expect(resolved).toBe(true); + }); + it('requires both onboarding complete and Snap platform ready when ensureOnboardingComplete is provided', async () => { const { rootMessenger, messenger } = setup(); const ensureOnboardingComplete = jest.fn().mockResolvedValue(undefined);