Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING: Use custom UI in snap_dialog #1051

Merged
merged 14 commits into from Dec 15, 2022
1 change: 1 addition & 0 deletions packages/examples/examples/bls-signer/package.json
Expand Up @@ -18,6 +18,7 @@
"clean": "rimraf 'dist/*'"
},
"dependencies": {
"@metamask/snaps-ui": "^0.26.2",
"eth-json-rpc-errors": "^1.1.0",
"noble-bls12-381": "^0.2.3"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/examples/examples/bls-signer/snap.manifest.json
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps-monorepo.git"
},
"source": {
"shasum": "qjjWC6kWkmjne8yYUCbzXT8M2A51dQkOkZLReVwJM50=",
"shasum": "WFEHEX72iejZA/ER6IF6nxhdZscMzdf2TcvNt1mkk1s=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand All @@ -17,7 +17,7 @@
}
},
"initialPermissions": {
"snap_confirm": {}
"snap_dialog": {}
},
"manifestVersion": "0.1"
}
12 changes: 8 additions & 4 deletions packages/examples/examples/bls-signer/src/index.js
@@ -1,3 +1,4 @@
const { panel, header, copyable } = require('@metamask/snaps-ui');
const { errors: rpcErrors } = require('eth-json-rpc-errors');
const bls = require('noble-bls12-381');

Expand Down Expand Up @@ -62,17 +63,20 @@ async function getPubKey() {
/**
* Displays a prompt to the user in the MetaMask UI.
*
* @param {string} header - A prompt, phrased as a question, no greater than 40
* @param {string} title - A prompt, phrased as a question, no greater than 40
* characters long.
* @param {string} [message] - Free-from text content, no greater than 1800
* characters long.
* @returns {Promise<boolean>} `true` if the user accepted the confirmation,
* and `false` otherwise.
*/
async function promptUser(header, message) {
async function promptUser(title, message) {
const response = await snap.request({
method: 'snap_confirm',
params: [{ prompt: header, textAreaContent: message }],
method: 'snap_dialog',
params: {
type: 'Confirmation',
content: panel([header(title), copyable(message)]),
},
});
return response;
}
4 changes: 2 additions & 2 deletions packages/rpc-methods/jest.config.js
Expand Up @@ -12,8 +12,8 @@ module.exports = deepmerge(baseConfig, {
global: {
branches: 75.18,
functions: 87.5,
lines: 88.67,
statements: 88.32,
lines: 88.61,
statements: 88.26,
},
},
testTimeout: 2500,
Expand Down
1 change: 1 addition & 0 deletions packages/rpc-methods/package.json
Expand Up @@ -29,6 +29,7 @@
"@metamask/browser-passworder": "^4.0.2",
"@metamask/key-tree": "^6.0.0",
"@metamask/permission-controller": "^1.0.1",
"@metamask/snaps-ui": "^0.26.2",
"@metamask/snaps-utils": "^0.26.2",
"@metamask/types": "^1.1.0",
"@metamask/utils": "^3.3.1",
Expand Down
231 changes: 35 additions & 196 deletions packages/rpc-methods/src/restricted/dialog.test.ts
@@ -1,4 +1,5 @@
import { PermissionType } from '@metamask/permission-controller';
import { heading, panel, text } from '@metamask/snaps-ui';

import {
dialogBuilder,
Expand Down Expand Up @@ -49,20 +50,17 @@ describe('implementation', () => {
method: 'snap_dialog',
params: {
type: DialogType.Alert,
fields: {
title: 'Foo',
description: 'Bar',
textAreaContent: 'Baz',
},
content: panel([heading('foo'), text('bar')]),
},
});

expect(hooks.showDialog).toHaveBeenCalledTimes(1);
expect(hooks.showDialog).toHaveBeenCalledWith('foo', DialogType.Alert, {
title: 'Foo',
description: 'Bar',
textAreaContent: 'Baz',
});
expect(hooks.showDialog).toHaveBeenCalledWith(
'foo',
DialogType.Alert,
panel([heading('foo'), text('bar')]),
undefined,
);
});
});

Expand All @@ -75,23 +73,16 @@ describe('implementation', () => {
method: 'snap_dialog',
params: {
type: DialogType.Confirmation,
fields: {
title: 'Foo',
description: 'Bar',
textAreaContent: 'Baz',
},
content: panel([heading('foo'), text('bar')]),
},
});

expect(hooks.showDialog).toHaveBeenCalledTimes(1);
expect(hooks.showDialog).toHaveBeenCalledWith(
'foo',
DialogType.Confirmation,
{
title: 'Foo',
description: 'Bar',
textAreaContent: 'Baz',
},
panel([heading('foo'), text('bar')]),
undefined,
);
});
});
Expand All @@ -105,20 +96,18 @@ describe('implementation', () => {
method: 'snap_dialog',
params: {
type: DialogType.Prompt,
fields: {
title: 'Foo',
description: 'Bar',
placeholder: 'Baz',
},
content: panel([heading('foo'), text('bar')]),
placeholder: 'foobar',
},
});

expect(hooks.showDialog).toHaveBeenCalledTimes(1);
expect(hooks.showDialog).toHaveBeenCalledWith('foo', DialogType.Prompt, {
title: 'Foo',
description: 'Bar',
placeholder: 'Baz',
});
expect(hooks.showDialog).toHaveBeenCalledWith(
'foo',
DialogType.Prompt,
panel([heading('foo'), text('bar')]),
'foobar',
);
});
});

Expand Down Expand Up @@ -169,12 +158,12 @@ describe('implementation', () => {

it.each([
{ type: DialogType.Alert },
{ type: DialogType.Alert, fields: null },
{ type: DialogType.Alert, fields: false },
{ type: DialogType.Alert, fields: '' },
{ type: DialogType.Alert, fields: 'abc' },
{ type: DialogType.Alert, fields: 2 },
{ type: DialogType.Alert, fields: [] },
{ type: DialogType.Alert, content: null },
{ type: DialogType.Alert, content: false },
{ type: DialogType.Alert, content: '' },
{ type: DialogType.Alert, content: 'abc' },
{ type: DialogType.Alert, content: 2 },
{ type: DialogType.Alert, content: [] },
])('rejects invalid fields', async (value) => {
const hooks = getMockDialogHooks();
const implementation = getDialogImplementation(hooks);
Expand All @@ -190,128 +179,9 @@ describe('implementation', () => {
);
});

it.each([undefined, null, false, 2, [], {}, new (class {})()])(
'rejects invalid titles',
async (value) => {
const hooks = getMockDialogHooks();
const implementation = getDialogImplementation(hooks);

await expect(
implementation({
context: { origin: 'foo' },
method: 'snap_dialog',
params: {
type: DialogType.Alert,
fields: {
title: value,
description: 'Bar',
textAreaContent: 'Baz',
} as any,
},
}),
).rejects.toThrow(
/Invalid params: At path: fields.title -- Expected a string, but received: .*\./u,
);
},
);

it('rejects titles with invalid length', async () => {
const hooks = getMockDialogHooks();
const implementation = getDialogImplementation(hooks);

await expect(
implementation({
context: { origin: 'foo' },
method: 'snap_dialog',
params: {
type: DialogType.Alert,
fields: {
title: '',
description: 'Bar',
textAreaContent: 'Baz',
},
},
}),
).rejects.toThrow(
'Invalid params: At path: fields.title -- Expected a string with a length between `1` and `40` but received one with a length of `0`.',
);
});

it.each([true, 2, [], {}, new (class {})()])(
'rejects invalid descriptions',
async (value) => {
const hooks = getMockDialogHooks();
const implementation = getDialogImplementation(hooks);

await expect(
implementation({
context: { origin: 'foo' },
method: 'snap_dialog',
params: {
type: DialogType.Alert,
fields: {
title: 'Foo',
description: value,
textAreaContent: 'Baz',
} as any,
},
}),
).rejects.toThrow(
/Invalid params: At path: fields.description -- Expected a string, but received: .*\./u,
);
},
);

it('rejects too long descriptions', async () => {
const hooks = getMockDialogHooks();
const implementation = getDialogImplementation(hooks);

await expect(
implementation({
context: { origin: 'foo' },
method: 'snap_dialog',
params: {
type: DialogType.Alert,
fields: {
title: 'Foo',
description: 'a'.repeat(141),
textAreaContent: 'Baz',
} as any,
},
}),
).rejects.toThrow(
'Invalid params: At path: fields.description -- Expected a string with a length between `1` and `140` but received one with a length of `141`.',
);
});

it.each([true, 2, [], {}, new (class {})()])(
'rejects invalid text area contents',
async (value) => {
const hooks = getMockDialogHooks();
const implementation = getDialogImplementation(hooks);

await expect(
implementation({
context: { origin: 'foo' },
method: 'snap_dialog',
params: {
type: DialogType.Alert,
fields: {
title: 'Foo',
description: 'Bar',
textAreaContent: value,
} as any,
},
}),
).rejects.toThrow(
/Invalid params: At path: fields\.textAreaContent -- Expected a string, but received: .*\./u,
);
},
);

it.each([true, 2, [], {}, new (class {})()])(
'rejects invalid placeholder contents',
async (value) => {
async (value: any) => {
GuillaumeRx marked this conversation as resolved.
Show resolved Hide resolved
const hooks = getMockDialogHooks();
const implementation = getDialogImplementation(hooks);

Expand All @@ -321,59 +191,32 @@ describe('implementation', () => {
method: 'snap_dialog',
params: {
type: DialogType.Prompt,
fields: {
title: 'Foo',
description: 'Bar',
placeholder: value,
} as any,
content: panel([heading('foo'), text('bar')]),
placeholder: value,
},
}),
).rejects.toThrow(
/Invalid params: At path: fields\.placeholder -- Expected a string, but received: .*\./u,
/Invalid params: At path: placeholder -- Expected a string, but received: .*\./u,
);
},
);

it('rejects too long text area contents', async () => {
it('rejects placeholders with invalid length', async () => {
const hooks = getMockDialogHooks();
const implementation = getDialogImplementation(hooks);

await expect(
implementation({
context: { origin: 'foo' },
method: 'snap_dialog',
params: {
type: DialogType.Alert,
fields: {
title: 'Foo',
description: 'Bar',
textAreaContent: 'a'.repeat(1801),
} as any,
},
}),
).rejects.toThrow(
'Invalid params: At path: fields.textAreaContent -- Expected a string with a length between `1` and `1800` but received one with a length of `1801`.',
);
});

it('rejects textAreaContent field for prompts', async () => {
const hooks = getMockDialogHooks();
const implementation = getDialogImplementation(hooks);
await expect(
implementation({
context: { origin: 'foo' },
method: 'snap_dialog',
params: {
type: DialogType.Prompt,
fields: {
title: 'Foo',
description: 'Bar',
textAreaContent: 'Baz',
} as any,
content: panel([heading('foo'), text('bar')]),
placeholder: '',
},
}),
).rejects.toThrow(
'Invalid params: Prompts may not specify a "textAreaContent" field.',
'Invalid params: At path: placeholder -- Expected a string with a length between `1` and `40` but received one with a length of `0`.',
);
});

Expand All @@ -388,12 +231,8 @@ describe('implementation', () => {
method: 'snap_dialog',
params: {
type,
fields: {
title: 'Foo',
description: 'Bar',
textAreaContent: 'Baz',
placeholder: 'Foobar',
} as any,
content: panel([heading('foo'), text('bar')]),
placeholder: 'foobar',
},
}),
).rejects.toThrow(
Expand Down