Skip to content

Commit 20ed5ef

Browse files
authored
feat: Add feature flag to treat local Snaps as preinstalled (#3523)
This adds a new feature flag `forcePreinstalledSnaps`, which makes the `SnapController` treat any local Snaps as preinstalled Snaps. It's intended to only be enabled in local Flask builds, and should not be enabled in any production builds. It can be used to simplify the development of preinstalled Snaps by using a local Snap instead. Related extension PR: MetaMask/metamask-extension#34022. Closes #3517.
1 parent 285c8d8 commit 20ed5ef

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8113,6 +8113,82 @@ describe('SnapController', () => {
81138113

81148114
snapController.destroy();
81158115
});
8116+
8117+
it('installs a local Snap as preinstalled Snap when `forcePreinstalledSnaps` is enabled', async () => {
8118+
const messenger = getSnapControllerMessenger();
8119+
const snapController = getSnapController(
8120+
getSnapControllerOptions({
8121+
messenger,
8122+
detectSnapLocation: loopbackDetect(),
8123+
featureFlags: {
8124+
allowLocalSnaps: true,
8125+
forcePreinstalledSnaps: true,
8126+
},
8127+
}),
8128+
);
8129+
8130+
await snapController.installSnaps(MOCK_ORIGIN, {
8131+
[MOCK_LOCAL_SNAP_ID]: {},
8132+
[MOCK_SNAP_ID]: {},
8133+
});
8134+
8135+
const localSnap = snapController.getExpect(MOCK_LOCAL_SNAP_ID);
8136+
expect(localSnap.preinstalled).toBe(true);
8137+
expect(localSnap.hideSnapBranding).toBe(true);
8138+
expect(localSnap.hidden).toBe(false);
8139+
8140+
const npmSnap = snapController.getExpect(MOCK_SNAP_ID);
8141+
expect(npmSnap.preinstalled).toBeUndefined();
8142+
expect(npmSnap.hideSnapBranding).toBeUndefined();
8143+
expect(npmSnap.hidden).toBeUndefined();
8144+
8145+
snapController.destroy();
8146+
});
8147+
8148+
it('updates an existing local Snap to be a preinstalled Snap when `forcePreinstalledSnaps` is enabled', async () => {
8149+
const snapObject = getPersistedSnapObject({
8150+
id: MOCK_LOCAL_SNAP_ID,
8151+
});
8152+
8153+
const location = new LoopbackLocation({
8154+
manifest: snapObject.manifest,
8155+
shouldAlwaysReload: true,
8156+
});
8157+
8158+
const messenger = getSnapControllerMessenger();
8159+
const snapController = getSnapController(
8160+
getSnapControllerOptions({
8161+
messenger,
8162+
detectSnapLocation: loopbackDetect(location),
8163+
featureFlags: {
8164+
allowLocalSnaps: true,
8165+
forcePreinstalledSnaps: true,
8166+
},
8167+
state: {
8168+
snaps: {
8169+
[MOCK_LOCAL_SNAP_ID]: snapObject,
8170+
},
8171+
},
8172+
}),
8173+
);
8174+
8175+
const localSnapBeforeUpdate =
8176+
snapController.getExpect(MOCK_LOCAL_SNAP_ID);
8177+
expect(localSnapBeforeUpdate.preinstalled).toBeUndefined();
8178+
expect(localSnapBeforeUpdate.hideSnapBranding).toBeUndefined();
8179+
expect(localSnapBeforeUpdate.hidden).toBeUndefined();
8180+
8181+
await snapController.installSnaps(MOCK_ORIGIN, {
8182+
[MOCK_LOCAL_SNAP_ID]: {},
8183+
});
8184+
8185+
const localSnap = snapController.getExpect(MOCK_LOCAL_SNAP_ID);
8186+
expect(localSnap.preinstalled).toBe(true);
8187+
expect(localSnap.hideSnapBranding).toBe(true);
8188+
expect(localSnap.hidden).toBe(false);
8189+
8190+
snapController.destroy();
8191+
});
81168192
});
81178193

81188194
it('throws if the Snap source code is too large', async () => {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ import {
187187
throttleTracking,
188188
withTimeout,
189189
isTrackableHandler,
190+
isLocalSnapId,
190191
} from '../utils';
191192

192193
export const controllerName = 'SnapController';
@@ -700,6 +701,14 @@ type FeatureFlags = {
700701
disableSnapInstallation?: boolean;
701702
rejectInvalidPlatformVersion?: boolean;
702703
useCaip25Permission?: boolean;
704+
705+
/**
706+
* Force any local Snap to be treated as a preinstalled Snap.
707+
*
708+
* This should only be used for local testing, and should not be enabled in
709+
* any production builds (including beta and Flask).
710+
*/
711+
forcePreinstalledSnaps?: boolean;
703712
};
704713

705714
type DynamicFeatureFlags = {
@@ -3117,10 +3126,20 @@ export class SnapController extends BaseController<
31173126
platformVersion: manifest.platformVersion,
31183127
});
31193128

3129+
const preinstalledArgs =
3130+
this.#featureFlags.forcePreinstalledSnaps && isLocalSnapId(snapId)
3131+
? {
3132+
preinstalled: true,
3133+
hideSnapBranding: true,
3134+
hidden: false,
3135+
}
3136+
: {};
3137+
31203138
return this.#set({
31213139
...args,
31223140
files: fetchedSnap,
31233141
id: snapId,
3142+
...preinstalledArgs,
31243143
});
31253144
})();
31263145
}

packages/snaps-controllers/src/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,3 +470,14 @@ export function isTrackableHandler(
470470
): handler is TrackableHandler {
471471
return TRACKABLE_HANDLERS.includes(handler as TrackableHandler);
472472
}
473+
474+
/**
475+
* Checks if the given Snap ID is a local Snap ID. This assumes the Snap ID is
476+
* validated before calling this function, as it only checks the prefix.
477+
*
478+
* @param snapId - The Snap ID to check.
479+
* @returns True if the Snap ID is a local Snap ID, false otherwise.
480+
*/
481+
export function isLocalSnapId(snapId: SnapId): boolean {
482+
return snapId.startsWith('local:');
483+
}

0 commit comments

Comments
 (0)