Skip to content

Commit fb11d7a

Browse files
MrtenzGuillaumeRx
andauthored
feat: Add onActive and onInactive lifecycle hooks (#3542)
This adds support for the `onActive` and `onInactive` lifecycle hooks. It's accomplished by exposing a new `setActive` action in the Snap controller, which should be called by the clients when the active status changes. Related to #3541. --------- Co-authored-by: Guillaume Roux <[email protected]>
1 parent 8c54f0c commit fb11d7a

File tree

11 files changed

+334
-27
lines changed

11 files changed

+334
-27
lines changed

packages/examples/packages/lifecycle-hooks/snap.manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snaps.git"
88
},
99
"source": {
10-
"shasum": "hTwghLmR+2f8zwjEcFujq/ojCDoMnQRcYBPMJ73IqGE=",
10+
"shasum": "/Wc+1sY2V4NwgUxQT81/nquwTBDsw2vdNEkkYd8g+0Q=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",
@@ -18,6 +18,7 @@
1818
},
1919
"initialPermissions": {
2020
"snap_dialog": {},
21+
"snap_notify": {},
2122
"endowment:lifecycle-hooks": {}
2223
},
2324
"platformVersion": "9.2.0",

packages/examples/packages/lifecycle-hooks/src/index.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
OnActiveHandler,
23
OnInstallHandler,
34
OnStartHandler,
45
OnUpdateHandler,
@@ -85,3 +86,43 @@ export const onUpdate: OnUpdateHandler = async () => {
8586
},
8687
});
8788
};
89+
90+
/**
91+
* Handle activation of the client. This handler is called when the client is
92+
* activated, and can be used to perform any initialization that is required.
93+
*
94+
* This handler is optional.
95+
*
96+
* @see https://docs.metamask.io/snaps/reference/entry-points/#onactive
97+
* @returns The JSON-RPC response.
98+
*/
99+
export const onActive: OnActiveHandler = async () => {
100+
return await snap.request({
101+
method: 'snap_notify',
102+
params: {
103+
type: 'inApp',
104+
message:
105+
'The client was activated, and the "onActive" handler was called.',
106+
},
107+
});
108+
};
109+
110+
/**
111+
* Handle deactivation of the client. This handler is called when the client
112+
* is deactivated, and can be used to perform any cleanup that is required.
113+
*
114+
* This handler is optional.
115+
*
116+
* @see https://docs.metamask.io/snaps/reference/entry-points/#oninactive
117+
* @returns The JSON-RPC response.
118+
*/
119+
export const onInactive = async () => {
120+
return await snap.request({
121+
method: 'snap_notify',
122+
params: {
123+
type: 'inApp',
124+
message:
125+
'The client was deactivated, and the "onInactive" handler was called.',
126+
},
127+
});
128+
};

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

Lines changed: 181 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
Caveat,
1111
SubjectPermissions,
1212
ValidPermission,
13+
CaveatConstraint,
1314
} from '@metamask/permission-controller';
1415
import { SubjectType } from '@metamask/permission-controller';
1516
import { providerErrors, rpcErrors } from '@metamask/rpc-errors';
@@ -10336,7 +10337,11 @@ describe('SnapController', () => {
1033610337

1033710338
rootMessenger.registerActionHandler(
1033810339
'PermissionController:getPermissions',
10339-
(origin) => {
10340+
(
10341+
origin,
10342+
): SubjectPermissions<
10343+
ValidPermission<TargetName, CaveatConstraint>
10344+
> => {
1034010345
if (origin === MOCK_SNAP_ID) {
1034110346
return {
1034210347
[SnapEndowments.LifecycleHooks]:
@@ -10443,7 +10448,7 @@ describe('SnapController', () => {
1044310448
await sleep(10);
1044410449

1044510450
expect(consoleErrorSpy).toHaveBeenCalledWith(
10446-
`Error when calling \`onStart\` lifecycle hook for Snap "npm:@metamask/example-snap": Test error in lifecycle hook.`,
10451+
`Error calling lifecycle hook "onStart" for Snap "npm:@metamask/example-snap": Test error in lifecycle hook.`,
1044710452
);
1044810453

1044910454
snapController.destroy();
@@ -12369,4 +12374,178 @@ describe('SnapController', () => {
1236912374
snapController.destroy();
1237012375
});
1237112376
});
12377+
12378+
describe('SnapController:setClientActive', () => {
12379+
it('calls the `onActive` lifecycle hook for all Snaps when called with `true`', async () => {
12380+
const rootMessenger = getControllerMessenger();
12381+
const messenger = getSnapControllerMessenger(rootMessenger);
12382+
12383+
rootMessenger.registerActionHandler(
12384+
'PermissionController:hasPermission',
12385+
() => true,
12386+
);
12387+
12388+
rootMessenger.registerActionHandler(
12389+
'PermissionController:getPermissions',
12390+
(
12391+
origin,
12392+
): SubjectPermissions<ValidPermission<string, CaveatConstraint>> => {
12393+
if (origin === MOCK_SNAP_ID) {
12394+
return {
12395+
[SnapEndowments.LifecycleHooks]: MOCK_LIFECYCLE_HOOKS_PERMISSION,
12396+
};
12397+
}
12398+
12399+
return {};
12400+
},
12401+
);
12402+
12403+
const manifest = getSnapManifest({
12404+
initialPermissions: {
12405+
[SnapEndowments.LifecycleHooks]: {},
12406+
},
12407+
});
12408+
12409+
const snapController = getSnapController(
12410+
getSnapControllerOptions({
12411+
messenger,
12412+
state: {
12413+
snaps: getPersistedSnapsState(getPersistedSnapObject({ manifest })),
12414+
},
12415+
}),
12416+
);
12417+
12418+
messenger.call('SnapController:setClientActive', true);
12419+
await sleep(10);
12420+
12421+
expect(messenger.call).toHaveBeenNthCalledWith(
12422+
2,
12423+
'PermissionController:hasPermission',
12424+
MOCK_SNAP_ID,
12425+
SnapEndowments.LifecycleHooks,
12426+
);
12427+
12428+
expect(messenger.call).toHaveBeenNthCalledWith(
12429+
3,
12430+
'PermissionController:hasPermission',
12431+
MOCK_SNAP_ID,
12432+
SnapEndowments.LifecycleHooks,
12433+
);
12434+
12435+
expect(messenger.call).toHaveBeenNthCalledWith(
12436+
4,
12437+
'PermissionController:getPermissions',
12438+
MOCK_SNAP_ID,
12439+
);
12440+
12441+
expect(messenger.call).toHaveBeenNthCalledWith(
12442+
5,
12443+
'ExecutionService:executeSnap',
12444+
expect.any(Object),
12445+
);
12446+
12447+
expect(messenger.call).toHaveBeenNthCalledWith(
12448+
6,
12449+
'ExecutionService:handleRpcRequest',
12450+
MOCK_SNAP_ID,
12451+
{
12452+
handler: HandlerType.OnActive,
12453+
origin: METAMASK_ORIGIN,
12454+
request: {
12455+
jsonrpc: '2.0',
12456+
id: expect.any(String),
12457+
method: HandlerType.OnActive,
12458+
},
12459+
},
12460+
);
12461+
12462+
snapController.destroy();
12463+
});
12464+
12465+
it('calls the `onInactive` lifecycle hook for all Snaps when called with `false`', async () => {
12466+
const rootMessenger = getControllerMessenger();
12467+
const messenger = getSnapControllerMessenger(rootMessenger);
12468+
12469+
rootMessenger.registerActionHandler(
12470+
'PermissionController:hasPermission',
12471+
() => true,
12472+
);
12473+
12474+
rootMessenger.registerActionHandler(
12475+
'PermissionController:getPermissions',
12476+
(
12477+
origin,
12478+
): SubjectPermissions<ValidPermission<string, CaveatConstraint>> => {
12479+
if (origin === MOCK_SNAP_ID) {
12480+
return {
12481+
[SnapEndowments.LifecycleHooks]: MOCK_LIFECYCLE_HOOKS_PERMISSION,
12482+
};
12483+
}
12484+
12485+
return {};
12486+
},
12487+
);
12488+
12489+
const manifest = getSnapManifest({
12490+
initialPermissions: {
12491+
[SnapEndowments.LifecycleHooks]: {},
12492+
},
12493+
});
12494+
12495+
const snapController = getSnapController(
12496+
getSnapControllerOptions({
12497+
messenger,
12498+
state: {
12499+
snaps: getPersistedSnapsState(getPersistedSnapObject({ manifest })),
12500+
},
12501+
}),
12502+
);
12503+
12504+
messenger.call('SnapController:setClientActive', false);
12505+
await sleep(10);
12506+
12507+
expect(messenger.call).toHaveBeenNthCalledWith(
12508+
2,
12509+
'PermissionController:hasPermission',
12510+
MOCK_SNAP_ID,
12511+
SnapEndowments.LifecycleHooks,
12512+
);
12513+
12514+
expect(messenger.call).toHaveBeenNthCalledWith(
12515+
3,
12516+
'PermissionController:hasPermission',
12517+
MOCK_SNAP_ID,
12518+
SnapEndowments.LifecycleHooks,
12519+
);
12520+
12521+
expect(messenger.call).toHaveBeenNthCalledWith(
12522+
4,
12523+
'PermissionController:getPermissions',
12524+
MOCK_SNAP_ID,
12525+
);
12526+
12527+
expect(messenger.call).toHaveBeenNthCalledWith(
12528+
5,
12529+
'ExecutionService:executeSnap',
12530+
expect.any(Object),
12531+
);
12532+
12533+
expect(messenger.call).toHaveBeenNthCalledWith(
12534+
6,
12535+
'ExecutionService:handleRpcRequest',
12536+
MOCK_SNAP_ID,
12537+
{
12538+
handler: HandlerType.OnInactive,
12539+
origin: METAMASK_ORIGIN,
12540+
request: {
12541+
jsonrpc: '2.0',
12542+
id: expect.any(String),
12543+
method: HandlerType.OnInactive,
12544+
},
12545+
},
12546+
);
12547+
12548+
snapController.destroy();
12549+
});
12550+
});
1237212551
});

0 commit comments

Comments
 (0)