Skip to content

Commit

Permalink
Add interactive UI documentation (#1166)
Browse files Browse the repository at this point in the history
* Update snaps-ui and snaps-types references

* Add interactive elements to Custom UI features

* Add interactive UI methods to Snaps API reference

* whatever

* update interactive-ui filename

* add onUserInput entry point

* add flask only label to interactive UI

* add feature card for interactive ui

* add interactive ui content page

* Add signature insights documentation (#1103)

* fix broken links

* remove signature stuff

* add places where interactive UI can be used

* Edit content

* fix link

* add interactive ui screenshots

* type > buttonType

* Re-add onUserInput entry point

* rearrange interactive ui methods and minor edits

* fix typo

* fix links

---------

Co-authored-by: Alexandra Tran <alexandratran@protonmail.com>
  • Loading branch information
ziad-saab and alexandratran authored Mar 28, 2024
1 parent 28b8725 commit 2860a35
Show file tree
Hide file tree
Showing 13 changed files with 493 additions and 36 deletions.
Binary file added snaps/assets/custom-ui-button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added snaps/assets/custom-ui-form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
199 changes: 180 additions & 19 deletions snaps/features/custom-ui.md → snaps/features/custom-ui/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ sidebar_position: 1

# Custom UI

You can display custom user interface (UI) components using the
You can display custom user interface (UI) components using the
[`@metamask/snaps-sdk`](https://github.com/MetaMask/snaps/tree/main/packages/snaps-sdk) module with
the following methods and entry points:

- [`snap_dialog`](../reference/snaps-api.md#snap_dialog)
- [`onTransaction`](../reference/entry-points.md#ontransaction)
- [`onHomePage`](../reference/entry-points.md#onhomepage)
- [`snap_dialog`](../../reference/snaps-api.md#snap_dialog)
- [`onTransaction`](../../reference/entry-points.md#ontransaction)
- [`onHomePage`](../../reference/entry-points.md#onhomepage)

To use custom UI, first install [`@metamask/snaps-sdk`](https://github.com/MetaMask/snaps/tree/main/packages/snaps-sdk)
using the following command:
Expand All @@ -22,7 +22,7 @@ yarn add @metamask/snaps-sdk

Then, whenever you're required to return a custom UI component, import the components from the
SDK and build your UI with them.
For example, to display a [`panel`](#panel) using [`snap_dialog`](../reference/snaps-api.md#snap_dialog):
For example, to display a [`panel`](#panel) using [`snap_dialog`](../../reference/snaps-api.md#snap_dialog):

```javascript title="index.js"
import { panel, heading, text } from "@metamask/snaps-sdk";
Expand Down Expand Up @@ -68,13 +68,65 @@ await snap.request({

<div class="row">
<div class="column">
<img src={require("../assets/custom-ui-address.png").default} alt="Address UI example" width="450px" style={{border: '1px solid #DCDCDC'}} />
<img src={require("../../assets/custom-ui-address.png").default} alt="Address UI example" width="450px" style={{border: '1px solid #DCDCDC'}} />
</div>
<div class="column">
<img src={require("../assets/custom-ui-address-tooltip.png").default} alt="Address tooltip UI example" width="450px" style={{border: '1px solid #DCDCDC'}} />
<img src={require("../../assets/custom-ui-address-tooltip.png").default} alt="Address tooltip UI example" width="450px" style={{border: '1px solid #DCDCDC'}} />
</div>
</div>

### `button`

:::flaskOnly
:::

Outputs a button that the user can select.
For use in [interactive UI](interactive-ui.md).

#### Parameters

An object containing:

- `value`: `string` - The text of the button.
- `buttonType`: `string` - (Optional) Possible values are `button` or `submit`.
The default is `button`.
- `name`: `string` - (Optional) The name that will be sent to [`onUserInput`](../../reference/entry-points.md#onuserinput)
when a user selects the button.
- `variant` - (Optional) Determines the appearance of the button.
Possible values are `primary` or `secondary`.
The default is `primary`.

#### Example

```javascript
import { button, panel, heading } from "@metamask/snaps-sdk";

const interfaceId = await snap.request({
method: "snap_createInterface",
params: {
ui: panel([
heading("Interactive interface"),
button({
value: "Click me",
name: "interactive-button",
}),
]),
},
});

await snap.request({
method: "snap_dialog",
params: {
type: "Alert",
id: interfaceId,
},
});
```

<p align="center">
<img src={require("../../assets/custom-ui-button.png").default} alt="Button UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

### `copyable`

Outputs a read-only text field with a copy-to-clipboard shortcut.
Expand All @@ -97,7 +149,7 @@ await snap.request({
```

<p align="center">
<img src={require("../assets/custom-ui-copyable.png").default} alt="Copyable UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
<img src={require("../../assets/custom-ui-copyable.png").default} alt="Copyable UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

### `divider`
Expand All @@ -122,7 +174,59 @@ module.exports.onHomePage = async () => {
```

<p align="center">
<img src={require("../assets/custom-ui-divider.png").default} alt="Divider UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
<img src={require("../../assets/custom-ui-divider.png").default} alt="Divider UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

### `form`

:::flaskOnly
:::

Outputs a form for use in [interactive UI](interactive-ui.md).

#### Parameters

An object containing:

- `name`: `string` - The name that will be sent to [`onUserInput`](../../reference/entry-points.md#onuserinput)
when a user interacts with the form.
- `children`: `array` - An array of [`input`](#input) or [`button`](#button) components.

#### Example

```js
import { input, button, form } from "@metamask/snaps-sdk";

const interfaceId = await snap.request({
method: "snap_createInterface",
params: {
ui: form({
name: "form-to-fill",
children: [
input({
name: "user-name",
placeholder: "Your name",
}),
button({
value: "Submit",
buttonType: "submit",
}),
],
}),
},
});

await snap.request({
method: "snap_dialog",
params: {
type: "Alert",
id: interfaceId,
},
});
```

<p align="center">
<img src={require("../../assets/custom-ui-form.png").default} alt="Form UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

### `heading`
Expand All @@ -147,7 +251,7 @@ module.exports.onHomePage = async () => {
```

<p align="center">
<img src={require("../assets/custom-ui-heading.png").default} alt="Divider UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
<img src={require("../../assets/custom-ui-heading.png").default} alt="Divider UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

### `image`
Expand All @@ -163,7 +267,7 @@ The SVG is rendered within an `<img>` tag, which prevents JavaScript or interact
being supported.

:::note
To disable image support, set the [`features.images`](../reference/cli/options.md#featuresimages)
To disable image support, set the [`features.images`](../../reference/cli/options.md#featuresimages)
configuration option to `false`.
The default is `true`.
:::
Expand All @@ -187,7 +291,64 @@ module.exports.onHomePage = async () => {
```

<p align="center">
<img src={require("../assets/custom-ui-image.png").default} alt="Divider UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
<img src={require("../../assets/custom-ui-image.png").default} alt="Divider UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

### `input`

:::flaskOnly
:::

Outputs an input component for use in [interactive UI](interactive-ui.md).

#### Parameters

An object containing:

- `name`: `string` - The name that will be used as a key to the event sent to
[`onUserInput`](../../reference/entry-points.md#onuserinput) when the containing form is submitted.
- `inputType`: `string` - (Optional) Type of input.
Possible values are `text`, `number`, or `password`.
The default is `text`.
- `placeholder`: `string` - (Optional) The text displayed when the input is empty.
- `label`: `string` (Optional) The text displayed alongside the input to label it.
- `value`: `string` (Optional) The default value of the input.

#### Example

```js
import { input, form } from "@metamask/snaps-sdk";

const interfaceId = await snap.request({
method: "snap_createInterface",
params: {
ui: form({
name: "form-to-fill",
children: [
input({
name: "user-name",
placeholder: "Your name",
}),
button({
value: "Submit",
buttonType: "submit",
}),
],
}),
},
});

await snap.request({
method: "snap_dialog",
params: {
type: "Alert",
id: interfaceId,
},
});
```

<p align="center">
<img src={require("../../assets/custom-ui-form.png").default} alt="Form UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

:::note
Expand Down Expand Up @@ -222,7 +383,7 @@ module.exports.onTransaction = async ({ transaction }) => {
```

<p align="center">
<img src={require("../assets/custom-ui-panel.png").default} alt="Panel UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
<img src={require("../../assets/custom-ui-panel.png").default} alt="Panel UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

### `row`
Expand All @@ -249,7 +410,7 @@ await snap.request({
```

<p align="center">
<img src={require("../assets/custom-ui-row.png").default} alt="Row UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
<img src={require("../../assets/custom-ui-row.png").default} alt="Row UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

### `spinner`
Expand All @@ -274,7 +435,7 @@ await snap.request({
```

<p align="center">
<img src={require("../assets/custom-ui-spinner.gif").default} alt="Spinner UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
<img src={require("../../assets/custom-ui-spinner.gif").default} alt="Spinner UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

### `text`
Expand All @@ -298,7 +459,7 @@ module.exports.onHomePage = async () => {
```

<p align="center">
<img src={require("../assets/custom-ui-heading.png").default} alt="Spinner UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
<img src={require("../../assets/custom-ui-heading.png").default} alt="Spinner UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

## Markdown
Expand All @@ -323,7 +484,7 @@ await snap.request({
```

<p align="center">
<img src={require("../assets/custom-ui-markdown.png").default} alt="Markdown UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
<img src={require("../../assets/custom-ui-markdown.png").default} alt="Markdown UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

## Links
Expand All @@ -348,7 +509,7 @@ module.exports.onHomePage = async () => {
```

<p align="center">
<img src={require("../assets/custom-ui-links.png").default} alt="Spinner UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
<img src={require("../../assets/custom-ui-links.png").default} alt="Spinner UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

## Emojis
Expand All @@ -373,7 +534,7 @@ await snap.request({
```

<p align="center">
<img src={require("../assets/custom-ui-emojis.png").default} alt="Emojis UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
<img src={require("../../assets/custom-ui-emojis.png").default} alt="Emojis UI example" width="450px" style={{border: "1px solid #DCDCDC"}} />
</p>

## Examples
Expand Down
67 changes: 67 additions & 0 deletions snaps/features/custom-ui/interactive-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
description: Display and update interactive user interfaces.
sidebar_position: 1
sidebar_custom_props:
flask_only: true
---

# Interactive UI

:::flaskOnly
:::

You can display interactive user interface (UI) components.
Interactive UI is an extension of [custom UI](index.md).
It allows interfaces returned from [`snap_dialog`](../../reference/snaps-api.md#snap_dialog),
[`onTransaction`](../../reference/entry-points.md#ontransaction), and
[`onHomePage`](../../reference/entry-points.md#onhomepage) to respond to user input.

The following interactive UI components are available:

- [`button`](index.md#button)
- [`form`](index.md#form)
- [`input`](index.md#input)

## Create an interactive interface

Create an interactive interface using the
[`snap_createInterface`](../../reference/snaps-api.md#snap_createinterface) method.
This method returns the ID of the created interface.
You can pass this ID to [`snap_dialog`](../../reference/snaps-api.md#snap_dialog), returned from
[`onTransaction`](../../reference/entry-points.md#ontransaction), or from
[`onHomePage`](../../reference/entry-points.md#onhomepage).

If you need to [update the interface](#update-an-interactive-interface) or
[get its state](#get-an-interactive-interfaces-state) at a future time, you should store its ID in
the Snap's storage.

## Update an interactive interface

To update an interactive interface that is still active, use the
[`snap_updateInterface`](../../reference/snaps-api.md#snap_updateinterface) method.
Pass the ID of the interface to be updated, and the new UI.

Updating an interface can be done as part of the
[`onUserInput`](../../reference/entry-points.md#onuserinput) entry point or as part of an
asynchronous process.

The following is an example flow:

1. The user activates an interactive interface to send Bitcoin funds to an address.
The initial interface contains an address input, an amount input, and a **Send funds** button.
2. The user fills the fields, and selects the **Send funds** button.
3. `onUserInput` is called, and the logic detects that the **Send funds** button was selected.
4. `snap_updateInterface` is called, replacing the **Send funds** button with a [`spinner`](index.md#spinner).
5. Custom logic sends the funds.
6. `snap_updateInterface` is called again, replacing the whole UI with a success message.

## Get an interactive interface's state

At any point you can retrieve an interactive interface's state.
To do this, call the [`snap_getInterfaceState`](../../reference/snaps-api.md#snap_getinterfacestate)
method with the ID of the interface.

## Example

See the [`@metamask/interactive-ui-example-snap`](https://github.com/MetaMask/snaps/tree/main/packages/examples/packages/interactive-ui)
package for a full example of implementing interactive UI.
9 changes: 9 additions & 0 deletions snaps/how-to/request-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ following to the manifest file:
}
```

:::note
All Snaps API methods except the following interactive UI methods require requesting permission in
the manifest file:

- [`snap_createInterface`](../reference/snaps-api.md#snap_createinterface)
- [`snap_getInterfaceState`](../reference/snaps-api.md#snap_getinterfacestate)
- [`snap_updateInterface`](../reference/snaps-api.md#snap_updateInterface)
:::

### Endowments

Endowments are a type of permission.
Expand Down
Loading

0 comments on commit 2860a35

Please sign in to comment.