diff --git a/docs/whats-new.md b/docs/whats-new.md index 97e92cec550..d14746b29a6 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -19,6 +19,8 @@ of the [MetaMask developer page](https://metamask.io/developer/). ## June 2024 +- Documented [Snaps custom UI with JSX](/snaps/features/custom-ui/with-jsx). + ([#1348](https://github.com/MetaMask/metamask-docs/pull/1348)) - Updated [React dapp with global state tutorial](/wallet/tutorials/react-dapp-global-state) with instructions for EIP-6963. ([#1330](https://github.com/MetaMask/metamask-docs/pull/1330)) diff --git a/services/index.md b/services/index.md index 5c6c9f4541a..bacbc387883 100644 --- a/services/index.md +++ b/services/index.md @@ -3,9 +3,9 @@ sidebar_label: Introduction sidebar_position: 1 --- -import CodeTerminal from '@site/src/components/CodeTerminal/CodeTerminal.jsx'; -import SectionAPIs from '@site/src/components/Sections/SectionAPIs.jsx'; -import SectionNetworks from '@site/src/components/Sections/SectionNetworks.jsx'; +import CodeTerminal from "@site/src/components/CodeTerminal/CodeTerminal.jsx"; +import SectionAPIs from "@site/src/components/Sections/SectionAPIs.jsx"; +import SectionNetworks from "@site/src/components/Sections/SectionNetworks.jsx"; # Build and scale your dapp using services diff --git a/snaps/assets/custom-ui-box.png b/snaps/assets/custom-ui-box.png new file mode 100644 index 00000000000..11e9b04de19 Binary files /dev/null and b/snaps/assets/custom-ui-box.png differ diff --git a/snaps/assets/custom-ui-dropdown-active.png b/snaps/assets/custom-ui-dropdown-active.png new file mode 100644 index 00000000000..6db3a940bc2 Binary files /dev/null and b/snaps/assets/custom-ui-dropdown-active.png differ diff --git a/snaps/assets/custom-ui-dropdown.png b/snaps/assets/custom-ui-dropdown.png new file mode 100644 index 00000000000..2f0bb094bf9 Binary files /dev/null and b/snaps/assets/custom-ui-dropdown.png differ diff --git a/snaps/assets/custom-ui-field.png b/snaps/assets/custom-ui-field.png new file mode 100644 index 00000000000..0ad7cd9400d Binary files /dev/null and b/snaps/assets/custom-ui-field.png differ diff --git a/snaps/features/cron-jobs.md b/snaps/features/cron-jobs.md index cc87cf056e3..92b4c32bc32 100644 --- a/snaps/features/cron-jobs.md +++ b/snaps/features/cron-jobs.md @@ -3,6 +3,9 @@ description: Schedule periodic actions for your users. sidebar_position: 1 --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Cron jobs You can schedule actions to run periodically at fixed times or intervals, also known as "cron jobs." @@ -40,8 +43,43 @@ Expose an [`onCronjob`](../reference/entry-points.md#oncronjob) entry point, whi the specified schedule with the requests defined in the `endowment:cronjob` permission. The following example handles the `execute` method specified in the previous example: + + + + +```tsx title="index.tsx" +import type { OnCronjobHandler } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +export const onCronjob: OnCronjobHandler = async ({ request }) => { + switch (request.method) { + case "execute": + // Cron jobs can execute any method that is available to the Snap. + return snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + Cron job + This dialog was triggered by a cron job. + + ), + }, + }); + + default: + throw new Error("Method not found."); + } +}; +``` + + + + ```typescript title="index.ts" -import type { OnCronjobHandler } from "@metamask/snaps-sdk" +import type { OnCronjobHandler } from "@metamask/snaps-sdk"; +import { panel, heading, text } from "@metamask/snaps-sdk"; export const onCronjob: OnCronjobHandler = async ({ request }) => { switch (request.method) { @@ -64,6 +102,9 @@ export const onCronjob: OnCronjobHandler = async ({ request }) => { } ``` + + + :::tip Access data from cron jobs When accessing [encrypted data](data-storage.md#2-use-encrypted-storage) from cron jobs, MetaMask requires the user to enter their password if the wallet is locked. diff --git a/snaps/features/custom-ui/dialogs.md b/snaps/features/custom-ui/dialogs.md index c4aacb4f82d..80831ad3d30 100644 --- a/snaps/features/custom-ui/dialogs.md +++ b/snaps/features/custom-ui/dialogs.md @@ -3,6 +3,9 @@ description: Display custom alert, confirmation, or prompt screens in MetaMask. sidebar_position: 2 --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Dialogs You can display a dialog in the MetaMask UI using the @@ -35,6 +38,31 @@ To display an alert that can only be acknowledged, call [`snap_dialog`](../../reference/snaps-api.md#snap_dialog) with `type: "alert"`. The following example displays custom UI that alerts the user when something happens in the system: + + + +```tsx title="index.tsx" +import { Box, Text, Heading } from "@metamask/snaps-sdk/jsx"; + +await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + Something happened in the system + The thing that happened is... + + ), + }, +}); + +// Code that should execute after the alert has been acknowledged. +``` + + + + ```javascript title="index.js" import { panel, text, heading } from "@metamask/snaps-sdk" @@ -52,6 +80,10 @@ await snap.request({ // Code that should execute after the alert has been acknowledged. ``` + + + +

Alert dialog example

@@ -63,6 +95,33 @@ To display a confirmation that can be accepted or rejected, call The following example displays custom UI that asks the user to confirm whether they would like to take an action: + + + +```tsx title="index.tsx" +import { Box, Text, Heading } from "@metamask/snaps-sdk/jsx"; + +const result = await snap.request({ + method: "snap_dialog", + params: { + type: "confirmation", + content: ( + + Would you like to take the action? + The action is... + + ), + }, +}); + +if (result === true) { + // Do the action. +} +``` + + + + ```javascript title="index.js" import { panel, text, heading } from "@metamask/snaps-sdk" @@ -82,6 +141,9 @@ if (result === true) { } ``` + + +

Confirmation dialog example

@@ -94,6 +156,32 @@ Prompt dialogs also accept a `placeholder` value that displays in the input fiel The following example displays custom UI that prompts the user to enter a wallet address: + + + +```tsx title="index.tsx" +import { Box, Text, Heading } from "@metamask/snaps-sdk/jsx"; + +const walletAddress = await snap.request({ + method: "snap_dialog", + params: { + type: "prompt", + content: ( + + What is the wallet address? + Please enter the wallet address to be monitored + + ), + placeholder: "0x123...", + }, +}); + +// walletAddress will be a string containing the address entered by the user. +``` + + + + ```javascript title="index.js" import { panel, text, heading } from "@metamask/snaps-sdk" @@ -112,6 +200,9 @@ const walletAddress = await snap.request({ // walletAddress will be a string containing the address entered by the user. ``` + + +

Prompt dialog example

diff --git a/snaps/features/custom-ui/home-pages.md b/snaps/features/custom-ui/home-pages.md index b7fcfc34e44..7fd7fe0285f 100644 --- a/snaps/features/custom-ui/home-pages.md +++ b/snaps/features/custom-ui/home-pages.md @@ -3,6 +3,9 @@ description: Display a dedicated UI page in MetaMask for your Snap. sidebar_position: 3 --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Home pages You can display a dedicated UI, or "home page," for your Snap within MetaMask. @@ -32,6 +35,28 @@ MetaMask calls this method when a user selects your Snap name in the Snaps menu. The following example displays custom UI that welcomes the user to the Snap's home page: + + + +```tsx title="index.tsx" +import type { OnHomePageHandler } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +export const onHomePage: OnHomePageHandler = async () => { + return { + content: ( + + Hello world! + Welcome to my Snap home page! + + ), + }; +}; +``` + + + + ```typescript title="index.ts" import type { OnHomePageHandler } from "@metamask/snaps-sdk" import { panel, text, heading } from "@metamask/snaps-sdk" @@ -46,6 +71,9 @@ export const onHomePage: OnHomePageHandler = async () => { } ``` + + +

Home page example

diff --git a/snaps/features/custom-ui/index.md b/snaps/features/custom-ui/index.md index e886f0fe169..517e9c23f4f 100644 --- a/snaps/features/custom-ui/index.md +++ b/snaps/features/custom-ui/index.md @@ -5,13 +5,18 @@ sidebar_position: 4 # Custom UI +:::caution +This version of custom UI is deprecated. If you're building a new Snaps project, +use [custom UI with JSX](./with-jsx). JSX is supported in MetaMask Extension and Flask version 12+. +::: + You can display custom user interface (UI) components using the [`@metamask/snaps-sdk`](https://github.com/MetaMask/snaps/tree/main/packages/snaps-sdk) module when implementing the following features: - [Dialogs](dialogs.md) - [Home pages](home-pages.md) -- [Transaction insights](../../reference/entry-points.md#ontransaction) +- [Transaction insights](../transaction-insights.md) - [Signature insights](../signature-insights.md) To use custom UI, first install [`@metamask/snaps-sdk`](https://github.com/MetaMask/snaps/tree/main/packages/snaps-sdk) @@ -309,7 +314,7 @@ An object containing: #### Example ```js -import { input, form } from "@metamask/snaps-sdk" +import { button, input, form } from "@metamask/snaps-sdk"; const interfaceId = await snap.request({ method: "snap_createInterface", diff --git a/snaps/features/custom-ui/interactive-ui.md b/snaps/features/custom-ui/interactive-ui.md index d3ff0a7a81f..7eb54e057d3 100644 --- a/snaps/features/custom-ui/interactive-ui.md +++ b/snaps/features/custom-ui/interactive-ui.md @@ -16,6 +16,14 @@ The following interactive UI components are available: - [`form`](index.md#form) - [`input`](index.md#input) +The following interactive UI JSX components are available: + +- [`Button`](with-jsx.md#button) +- [`Dropdown`](with-jsx.md#dropdown) +- [`Field`](with-jsx.md#field) +- [`Form`](with-jsx.md#form) +- [`Input`](with-jsx.md#input) + ## Create an interactive interface Create an interactive interface using the diff --git a/snaps/features/custom-ui/with-jsx.md b/snaps/features/custom-ui/with-jsx.md new file mode 100644 index 00000000000..efc28a828e6 --- /dev/null +++ b/snaps/features/custom-ui/with-jsx.md @@ -0,0 +1,687 @@ +--- +description: Display custom user interface components using JSX. +sidebar_position: 4 +--- + +# Custom UI with JSX + +You can display custom user interface (UI) JSX components using the +[`@metamask/snaps-sdk`](https://github.com/MetaMask/snaps/tree/main/packages/snaps-sdk) module when +implementing the following features: + +- [Dialogs](dialogs.md) +- [Home pages](home-pages.md) +- [Transaction insights](../transaction-insights.md) +- [Signature insights](../signature-insights.md) + +:::note +JSX is supported in MetaMask Extension and Flask version 12+. New UI components will be added as JSX components. The previous function-based library is deprecated. +::: + +To use custom UI with JSX, first install [`@metamask/snaps-sdk`](https://github.com/MetaMask/snaps/tree/main/packages/snaps-sdk) +using the following command: + +```bash +yarn add @metamask/snaps-sdk +``` + +Then, whenever you're required to return a custom UI JSX component, import the components from the +SDK at `@metamask/snaps-sdk/jsx` and build your UI with them. +For example, to display a [`Box`](#box) (the [`panel`](./index.md#panel) function equivalent) using [`snap_dialog`](../../reference/snaps-api.md#snap_dialog): + +```javascript title="index.jsx" +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + Alert heading + Something happened in the system. + + ), + }, +}); +``` + +:::note +Note that JSX can only be used in `.jsx` or `.tsx` files. +::: + +## Components + +The following custom UI JSX components are available: + +### `Address` + +Outputs a formatted text field for an Ethereum address. +The address is automatically displayed with a jazzicon and truncated value. +Hovering the address shows the full value in a tooltip. + +#### Example + +```javascript title="index.jsx" +import { Box, Heading, Address } from "@metamask/snaps-sdk/jsx"; + +await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + Are you sure you want to send tokens to this address? +
0x000000000000000000000000000000000000dEaD
+
+ ), + }, +}); +``` + +
+
+ Address UI example +
+
+ Address tooltip UI example +
+
+ +### `Bold` + +Outputs bold text. + +#### Example + +```javascript title="index.jsx" +import { Box, Heading, Text, Bold } from "@metamask/snaps-sdk/jsx"; + +await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + Hello world! + + This is bold. + + + ), + }, +}); +``` + +### `Box` + +Outputs a box, which can be used as a container for other components. + +#### Props + +- `direction` - (Optional) The direction in which elements flow inside the box. + Possible values are `"horizontal"` or `"vertical"`. + The default is `"vertical"`. +- `alignment` - (Optional) The alignment of the elements inside the box. + Possible values are `"start"`, `"center"`, `"end"`, `"space-between"`, or `"space-around"`. + The default is `"start"`. + +#### Example + +```javascript title="index.jsx" +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +module.exports.onHomePage = async () => { + return { + content: ( + + Features + + Feature 1 + Feature 2 + Feature 3 + + + ), + }; +}; +``` + +

+Box UI example +

+ +### `Button` + +Outputs a button that the user can select. +For use in [interactive UI](interactive-ui.md). + +#### Props + +- `children`: `string` - The text of the button. +- `type` - (Optional) The type of button. + 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 { Box, Heading, Button } from "@metamask/snaps-sdk/jsx"; + +const interfaceId = await snap.request({ + method: "snap_createInterface", + params: { + ui: ( + + Interactive interface + + + ), + }, +}); + +await snap.request({ + method: "snap_dialog", + params: { + type: "Alert", + id: interfaceId, + }, +}); + +``` + +

+Button UI example +

+ +### `Copyable` + +Outputs a read-only text field with a copy-to-clipboard shortcut. + +#### Example + +```javascript title="index.jsx" +import { Box, Text, Copyable } from "@metamask/snaps-sdk/jsx"; + +await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + Your address: + 0x000000000000000000000000000000000000dEaD + + ), + }, +}); +``` + +

+Copyable UI example +

+ +### `Divider` + +Outputs a horizontal divider. + +#### Example + +```javascript title="index.jsx" +import { Box, Heading, Divider, Text } from "@metamask/snaps-sdk/jsx"; + +module.exports.onHomePage = async () => { + return { + content: ( + + Hello world! + + Welcome to my Snap home page! + + ), + }; +}; +``` + +

+Divider UI example +

+ +### `Dropdown` + +Outputs a dropdown for use in [interactive UI](interactive-ui.md). + +#### Props + +- `name`: `string` - The name sent to [`onUserInput`](../../reference/entry-points.md#onuserinput). +- `children`: `Option[]` - One or more `Option` components with the following props: + - `value`: `string` - The value sent to [`onUserInput`](../../reference/entry-points.md#onuserinput). + - `children`: `string` - The text displayed in the dropdown for that option. + +#### Example + +```js +import { Box, Text, Dropdown } from "@metamask/snaps-sdk/jsx"; + +const interfaceId = await snap.request({ + method: "snap_createInterface", + params: { + ui: ( + + Pick a currency + + + + + + ), + }, +}); + +await snap.request({ + method: "snap_dialog", + params: { + type: "Alert", + id: interfaceId, + }, +}); +``` + +
+
+ Dropdown UI example +
+
+ Active dropdown UI example +
+
+ +### `Field` + +Outputs a form field, wrapping a [`Dropdown`](#dropdown) or [`Input`](#input) to give it a label and optional error. + +#### Props + +- `label`: `string` - The label for the wrapped element. +- `error`: `string` - Any error for the wrapped element. Setting this changes the styling of the wrapped element to show that there is an error. +- `children` - The [`Dropdown`](#dropdown) or [`Input`](#input) element to be wrapped. + +#### Example + +```js +import { Field, Form, Input, Button } from "@metamask/snaps-sdk/jsx"; + +const interfaceId = await snap.request({ + method: "snap_createInterface", + params: { + ui: ( +
+ + + + +
+ ), + }, +}); + +await snap.request({ + method: "snap_dialog", + params: { + type: "Alert", + id: interfaceId, + }, +}); +``` + +

+Field example +

+ +### `Form` + +Outputs a form for use in [interactive UI](interactive-ui.md). + +#### Props + +- `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 { Form, Input, Button } from "@metamask/snaps-sdk/jsx"; + +const interfaceId = await snap.request({ + method: "snap_createInterface", + params: { + ui: ( +
+ + +
+ ), + }, +}); + +await snap.request({ + method: "snap_dialog", + params: { + type: "Alert", + id: interfaceId, + }, +}); +``` + +

+Form UI example +

+ +### `Heading` + +Outputs a heading. +This is useful for [`Box`](#box) titles. + +#### Example + +```javascript title="index.jsx" +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +module.exports.onHomePage = async () => { + return { + content: ( + + Hello world! + Welcome to my Snap home page! + + ), + }; +}; +``` + +

+Divider UI example +

+ +### `Image` + +Outputs an image. +This component takes an inline SVG. +It does not support remote URLs. + +You can import SVG, PNG, and JPEG files using an import statement. +These files are automatically imported as SVG strings, so you can pass them directly to the `Image` component. + +The SVG is rendered within an `` tag, which prevents JavaScript or interaction events from +being supported. + +:::note +To disable image support, set the [`features.images`](../../reference/cli/options.md#featuresimages) +configuration option to `false`. +The default is `true`. +::: + +#### Props + +- `src`: `string` - An inline SVG. +- `alt`: `string` - An optional alternative text for the image. + +#### Example + +```javascript title="index.jsx" +import { Box, Heading, Text, Image } from "@metamask/snaps-sdk/jsx"; +import svgIcon from "./path/to/icon.svg"; + +module.exports.onHomePage = async () => { + return { + content: ( + + Hello world! + Welcome to my Snap home page! + + + ), + }; +}; +``` + +

+Divider UI example +

+ +:::note +See the [`@metamask/images-example-snap`](https://github.com/MetaMask/snaps/tree/main/packages/examples/packages/images) +package for a full example of implementing images. +::: + +### `Input` + +Outputs an input component for use in [interactive UI](interactive-ui.md). + +#### Props + +- `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. +- `type` - (Optional) The 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 { Form, Input, Button } from "@metamask/snaps-sdk/jsx"; + +const interfaceId = await snap.request({ + method: "snap_createInterface", + params: { + ui: ( +
+ + +
+ ), + }, +}); + +await snap.request({ + method: "snap_dialog", + params: { + type: "Alert", + id: interfaceId, + }, +}); +``` + +

+Form UI example +

+ +### `Italic` + +Outputs italic text. + +#### Example + +```javascript title="index.jsx" +import { Box, Heading, Text, Italic } from "@metamask/snaps-sdk/jsx"; + +await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + Hello world! + + This is italic. + + + ), + }, +}); +``` + +### `Link` + +Outputs a clickable link. + +#### Props + +- `href`: `string` - The URL to point to. +- `children`: `Array` - The link text. + +#### Example + +```javascript title="index.jsx" +import { Box, Heading, Link, Text } from "@metamask/snaps-sdk/jsx"; + +module.exports.onHomePage = async () => { + return { + content: ( + + Hello world! + + Download MetaMask. + + + Read the MetaMask docs at MetaMask docs. + + + ), + }; +}; +``` + +

+Links UI example +

+ +### `Row` + +Outputs a row with a label and value, which can be used for key-value data. + +#### Props + +- `label`: `string` - The label of the row. +- `variant` - (Optional) The variant of the label. + Possible values are `"default"`, `"error"`, or `"warning"`. +- `children` - The value of the row, which can be a [`Text`](#text), [`Image`](#image), or + [`Address`](#address) component. + +#### Example + +```javascript title="index.jsx" +import { Box, Row, Text, Address } from "@metamask/snaps-sdk/jsx"; + +await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + +
0x000000000000000000000000000000000000dEaD
+
+ + 1.78 ETH + +
+ ), + }, +}); +``` + +

+Row UI example +

+ +### `Spinner` + +Outputs a loading indicator. + +#### Example + +```javascript title="index.jsx" +import { Box, Heading, Spinner } from "@metamask/snaps-sdk/jsx"; + +await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + Please wait... + + + ), + }, +}); +``` + +

+Spinner UI example +

+ +### `Text` + +Outputs text. + +#### Example + +```javascript title="index.jsx" +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +module.exports.onHomePage = async () => { + return { + content: ( + + Hello world! + Welcome to my Snap home page! + + ), + }; +}; +``` + +

+Text UI example +

+ +## Emojis + +Text-based components (such as [`Heading`](#heading) and [`Text`](#text)) accept emojis. + +#### Example + +```javascript title="index.jsx" +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + Hello world! + This is an apple 🍎 and this is an orange 🍊. + + ), + }, +}); +``` + +

+Emojis UI example +

\ No newline at end of file diff --git a/snaps/features/lifecycle-hooks.md b/snaps/features/lifecycle-hooks.md index ce7db582ddd..f748b857f87 100644 --- a/snaps/features/lifecycle-hooks.md +++ b/snaps/features/lifecycle-hooks.md @@ -3,6 +3,9 @@ sidebar_position: 6 description: Call an action when your Snap is installed or updated. --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Lifecycle hooks You can implement lifecycle hooks to automatically run an action, such as displaying a dialog or @@ -30,6 +33,35 @@ For example, you can use `onInstall` to perform any initialization that is requi The following example displays an [alert dialog](custom-ui/dialogs.md#display-an-alert-dialog) upon installation: + + + + +```tsx title="index.tsx" +import type { OnInstallHandler } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +export const onInstall: OnInstallHandler = async () => { + await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + Installation successful + + To use this Snap, visit the companion dapp at metamask.io. + + + ), + }, + }); +}; +``` + + + + ```typescript title="index.ts" import type { OnInstallHandler } from "@metamask/snaps-sdk" import { heading, panel, text } from "@metamask/snaps-sdk" @@ -50,6 +82,9 @@ export const onInstall: OnInstallHandler = async () => { } ``` + + + ### 3. Run an action on update To run an action when a user updates your Snap, expose the @@ -58,6 +93,34 @@ For example, you can use `onUpdate` to perform any migrations that are required The following example displays an [alert dialog](custom-ui/dialogs.md#display-an-alert-dialog) upon update: + + + + +```tsx title="index.tsx" +import type { OnUpdateHandler } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +export const onUpdate: OnUpdateHandler = async () => { + await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + Update successful + New features added in this version: + Added a dialog that appears when updating. + + ), + }, + }); +}; +``` + + + + ```typescript title="index.ts" import type { OnUpdateHandler } from "@metamask/snaps-sdk" import { heading, panel, text } from "@metamask/snaps-sdk" @@ -77,6 +140,9 @@ export const onUpdate: OnUpdateHandler = async () => { } ``` + + + ## Example See the [`@metamask/lifecycle-hooks-example-snap`](https://github.com/MetaMask/snaps/tree/main/packages/examples/packages/lifecycle-hooks) diff --git a/snaps/features/signature-insights.md b/snaps/features/signature-insights.md index dc06781a54b..4c70c39d575 100644 --- a/snaps/features/signature-insights.md +++ b/snaps/features/signature-insights.md @@ -124,6 +124,36 @@ level of `SeverityLevel.Critical`. The following is an example implementation of `onSignature`: + + + +```tsx title="index.tsx" +import type { OnSignatureHandler, SeverityLevel } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +export const onSignature: OnSignatureHandler = async ({ + signature, + signatureOrigin, +}) => { + const insights = /* Get insights based on custom logic */; + return { + content: ( + + My Signature Insights + Here are the insights: + {insights.map((insight) => ( + {insight.value} + ))} + + ), + severity: SeverityLevel.Critical, + }; +}; +``` + + + + ```typescript title="index.ts" import type { OnSignatureHandler, SeverityLevel } from "@metamask/snaps-sdk"; import { panel, heading, text } from "@metamask/snaps-sdk"; @@ -144,6 +174,9 @@ export const onSignature: OnSignatureHandler = async ({ }; ``` + + + When your Snap returns a signature insight with a `severity` of `SeverityLevel.Critical`, the custom UI displays in a modal after the user selects the **Sign** button. For example: diff --git a/snaps/features/transaction-insights.md b/snaps/features/transaction-insights.md index 716ea9df48c..99f8ffd28bb 100644 --- a/snaps/features/transaction-insights.md +++ b/snaps/features/transaction-insights.md @@ -4,6 +4,9 @@ toc_max_heading_level: 4 sidebar_position: 13 --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Transaction insights You can provide transaction insights in MetaMask's transaction confirmation window before a user @@ -43,6 +46,36 @@ handler method. The following is an example implementation of `onTransaction`: + + + +```tsx title="index.tsx" +import type { OnTransactionHandler } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +export const onTransaction: OnTransactionHandler = async ({ + transaction, + chainId, + transactionOrigin, +}) => { + const insights = /* Get insights */; + return { + content: ( + + My Transaction Insights + Here are the insights: + {insights.map((insight) => ( + {insight.value} + ))} + + ), + }; +}; +``` + + + + ```typescript title="index.ts" import type { OnTransactionHandler } from "@metamask/snaps-sdk"; import { panel, heading, text } from "@metamask/snaps-sdk"; @@ -63,6 +96,10 @@ export const onTransaction: OnTransactionHandler = async ({ }; ``` + + + + The Snap tab in the transaction confirmation window displays the transaction insights:

@@ -77,6 +114,38 @@ The Snap tab in the transaction confirmation window displays the transaction ins A Snap providing transaction insights can return an optional severity level of `"critical"`. MetaMask shows a modal with the warning before the user can confirm the transaction. + + + +```tsx title="index.tsx" +import type { OnTransactionHandler } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +export const onTransaction: OnTransactionHandler = async ({ + transaction, + chainId, + transactionOrigin, +}) => { + const insights = /* Get insights */; + return { + content: ( + + My Transaction Insights + Here are the insights: + {insights.map((insight) => ( + {insight.value} + ))} + + ), + // highlight-next-line + severity: "critical", + }; +}; +``` + + + + ```typescript title="index.ts" import type { OnTransactionHandler } from "@metamask/snaps-sdk"; import { panel, heading, text } from "@metamask/snaps-sdk"; @@ -99,6 +168,9 @@ export const onTransaction: OnTransactionHandler = async ({ }; ``` + + +

Transaction insights warning

diff --git a/snaps/get-started/quickstart.md b/snaps/get-started/quickstart.md index b58e51c8250..10fefbbec4d 100644 --- a/snaps/get-started/quickstart.md +++ b/snaps/get-started/quickstart.md @@ -3,7 +3,9 @@ description: Get started quickly using the create-snap starter kit. sidebar_position: 2 --- -import YoutubeEmbed from '@site/src/components/YoutubeEmbed'; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import YoutubeEmbed from "@site/src/components/YoutubeEmbed"; # Snaps quickstart @@ -89,6 +91,50 @@ You can customize your Snap by editing `index.ts` in the `packages/snap/src` fol `index.ts` contains an example request that uses the [`snap_dialog`](../reference/snaps-api.md#snapdialog) method to display a custom confirmation screen: + + + +```tsx title="index.tsx" +import type { OnRpcRequestHandler } from "@metamask/snaps-sdk"; +import { Box, Text, Bold } from "@metamask/snaps-sdk/jsx"; + +/** + * Handle incoming JSON-RPC requests, sent through wallet_invokeSnap. + * + * @param args - The request handler arguments as an object. + * @param args.origin - The origin of the request, e.g., the website that invoked the Snap. + * @param args.request - A validated JSON-RPC request object. + * @returns The result of snap_dialog. + * @throws If the request method is not valid for this Snap. + */ +export const onRpcRequest: OnRpcRequestHandler = async ({ + origin, + request, +}) => { + switch (request.method) { + case "hello": + return snap.request({ + method: "snap_dialog", + params: { + type: "confirmation", + content: ( + + Hello, {origin}! + This custom confirmation is just for display purposes. + But you can edit the Snap source code to make it do something, if you want to! + + ), + }, + }); + default: + throw new Error("Method not found."); + } +}; +``` + + + + ```ts title="index.ts" import type { OnRpcRequestHandler } from "@metamask/snaps-sdk" import { panel, text } from "@metamask/snaps-sdk" @@ -127,6 +173,9 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ } ``` + + + Edit the text in any `text()` component and select the **Reconnect** button on the front-end to re-install the Snap. diff --git a/snaps/how-to/use-environment-variables.md b/snaps/how-to/use-environment-variables.md index 67ac057db4f..16ba0abec71 100644 --- a/snaps/how-to/use-environment-variables.md +++ b/snaps/how-to/use-environment-variables.md @@ -91,19 +91,47 @@ Snaps CLI: 3. You can also use environment variables directly in your Snap. For example: - ```typescript title="index.ts" - import { panel, text, heading } from "@metamask/snaps-sdk" - - await snap.request({ - method: "snap_dialog", - params: { - type: "alert", - content: panel([ - heading("This custom alert is just for display purposes."), - text( - `SNAP_ENV is ${process.env.SNAP_ENV}, PUBLIC_KEY is ${process.env.PUBLIC_KEY}` - ), - ]), - }, - }) - ``` + + + + ```tsx title="index.tsx" + import { Box, Text, Heading } from "@metamask/snaps-sdk/jsx"; + + await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + This custom alert is just for display purposes. + + SNAP_ENV is {process.env.SNAP_ENV}, PUBLIC_KEY is {process.env.PUBLIC_KEY} + + + ), + }, + }); + ``` + + + + + ```typescript title="index.ts" + import { panel, text, heading } from "@metamask/snaps-sdk" + + await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: panel([ + heading("This custom alert is just for display purposes."), + text( + `SNAP_ENV is ${process.env.SNAP_ENV}, PUBLIC_KEY is ${process.env.PUBLIC_KEY}` + ), + ]), + }, + }) + ``` + + + diff --git a/snaps/learn/tutorials/gas-estimation.md b/snaps/learn/tutorials/gas-estimation.md index 5ac32b8bf92..da8a3ba1f6e 100644 --- a/snaps/learn/tutorials/gas-estimation.md +++ b/snaps/learn/tutorials/gas-estimation.md @@ -3,6 +3,9 @@ description: Create a Snap that estimates gas fees. sidebar_position: 1 --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Create a Snap to estimate gas fees This tutorial walks you through creating a Snap that estimates gas fees. @@ -175,26 +178,51 @@ only one method, `hello`. For the `hello` method, the handler returns a call to MetaMask with the parameters to display a dialog, and passes some static strings. -Since `getFees()` returns a promise, you must use `then()` to resolve it in your `hello` method. Update the `hello` method with the following code: + + + +```tsx title="index.tsx" +case "hello": + const fees = await getFees(); + return snap.request({ + method: 'snap_dialog', + params: { + type: "alert", + content: ( + + Hello, {origin}! + Current gas fee estimates: + {fees} + + ), + } + }); +``` + + + + ```typescript title="index.ts" case "hello": - return getFees().then(fees => { - return snap.request({ - method: 'snap_dialog', - params: { - type: "alert", - content: panel([ - text(`Hello, **${origin}**!`), - text("Current gas fee estimates:"), - copyable(fees), - ]), - } - }); + const fees = await getFees(); + return snap.request({ + method: 'snap_dialog', + params: { + type: "alert", + content: panel([ + text(`Hello, **${origin}**!`), + text("Current gas fee estimates:"), + copyable(fees), + ]), + } }); ``` + + + ### 5. Build and test the Snap To build and test your Snap: diff --git a/snaps/learn/tutorials/transaction-insights.md b/snaps/learn/tutorials/transaction-insights.md index d5452462667..f39241876f6 100644 --- a/snaps/learn/tutorials/transaction-insights.md +++ b/snaps/learn/tutorials/transaction-insights.md @@ -3,6 +3,9 @@ description: Create a Snap that provides transaction insights. sidebar_position: 2 --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Create a Snap to calculate gas fee percentages This tutorial walks you through creating a Snap that calculates the percentage of gas fees that @@ -100,6 +103,58 @@ You do not need any permissions other than `endowment:transaction-insight` and ` To calculate and display the gas fees a user would pay as a percentage of their outgoing transaction, replace the code in `packages/snap/src/index.ts` with the following: + + + +```tsx title="index.tsx" +import type { OnTransactionHandler } from "@metamask/snaps-sdk"; +import { Box, Heading, Text, Bold } from "@metamask/snaps-sdk/jsx"; + +// Handle outgoing transactions. +export const onTransaction: OnTransactionHandler = async ({ transaction }) => { + + // Use the Ethereum provider to fetch the gas price. + const currentGasPrice = await ethereum.request({ + method: "eth_gasPrice", + }) as string; + + // Get fields from the transaction object. + const transactionGas = parseInt(transaction.gas as string, 16); + const currentGasPriceInWei = parseInt(currentGasPrice ?? "", 16); + const maxFeePerGasInWei = parseInt(transaction.maxFeePerGas as string, 16); + const maxPriorityFeePerGasInWei = parseInt( + transaction.maxPriorityFeePerGas as string, + 16, + ); + + // Calculate gas fees the user would pay. + const gasFees = Math.min( + maxFeePerGasInWei * transactionGas, + (currentGasPriceInWei + maxPriorityFeePerGasInWei) * transactionGas, + ); + + // Calculate gas fees as percentage of transaction. + const transactionValueInWei = parseInt(transaction.value as string, 16); + const gasFeesPercentage = (gasFees / (gasFees + transactionValueInWei)) * 100; + + // Display percentage of gas fees in the transaction insights UI. + return { + content: ( + + Transaction insights Snap + + As set up, you are paying {gasFeesPercentage.toFixed(2)}% + in gas fees for this transaction. + + + ), + }; +}; +``` + + + + ```typescript title="index.ts" import type { OnTransactionHandler } from "@metamask/snaps-sdk" import { heading, panel, text } from "@metamask/snaps-sdk" @@ -143,6 +198,9 @@ export const onTransaction: OnTransactionHandler = async ({ transaction }) => { } ``` + + + :::tip If you have previously developed a dapp, you're likely familiar with accessing the Ethereum provider using `window.ethereum`. In a Snap, the `window` object is not available. @@ -189,6 +247,27 @@ To build and test your Snap: The Snap should display a gas fee percentage for ETH transfers initiated by the user. For contract interactions, add the following code to the beginning of the `onTransaction` entry point: + + + +```tsx title="index.tsx" +if (typeof transaction.data === "string" && transaction.data !== "0x") { + return { + content: ( + + Percent Snap + + This Snap only provides transaction insights for simple ETH transfers. + + + ), + }; +} +``` + + + + ```typescript title="index.ts" if (typeof transaction.data === "string" && transaction.data !== "0x") { return { @@ -202,6 +281,9 @@ if (typeof transaction.data === "string" && transaction.data !== "0x") { } ``` + + + ### 6. Next steps The initial project has generic names in multiple places. diff --git a/snaps/reference/entry-points.md b/snaps/reference/entry-points.md index cd25bf0edb8..2b6765a7bd2 100644 --- a/snaps/reference/entry-points.md +++ b/snaps/reference/entry-points.md @@ -100,24 +100,26 @@ One of the following: #### Example - + -```typescript title="index.ts" -import type { OnHomePageHandler } from "@metamask/snaps-sdk" -import { panel, text, heading } from "@metamask/snaps-sdk" +```tsx title="index.tsx" +import type { OnHomePageHandler } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; export const onHomePage: OnHomePageHandler = async () => { return { - content: panel([ - heading("Hello world!"), - text("Welcome to my Snap home page!"), - ]), - } -} + content: ( + + Hello world! + Welcome to my Snap home page! + + ), + }; +}; ``` - + ```js title="index.js" import { panel, text, heading } from "@metamask/snaps-sdk" @@ -153,30 +155,32 @@ None. #### Example - + -```typescript title="index.ts" -import type { OnInstallHandler } from "@metamask/snaps-sdk" -import { heading, panel, text } from "@metamask/snaps-sdk" +```tsx title="index.tsx" +import type { OnInstallHandler } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; export const onInstall: OnInstallHandler = async () => { await snap.request({ method: "snap_dialog", params: { type: "alert", - content: panel([ - heading("Thank you for installing my Snap"), - text( - "To use this Snap, visit the companion dapp at [metamask.io](https://metamask.io)." - ), - ]), + content: ( + + Thank you for installing my Snap + + To use this Snap, visit the companion dapp at metamask.io. + + + ), }, - }) -} + }); +}; ``` - + ```js title="index.js" import { heading, panel, text } from "@metamask/snaps-sdk" @@ -200,6 +204,7 @@ module.exports.onInstall = async () => { + ## `onKeyringRequest` To implement the [Account Management API](keyring-api/account-management/index.md) to integrate @@ -428,11 +433,11 @@ An object containing: #### Example - + -```typescript title="index.ts" +```tsx title="index.tsx" import type { OnSignatureHandler, SeverityLevel } from "@metamask/snaps-sdk"; -import { panel, heading, text } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; export const onSignature: OnSignatureHandler = async ({ signature, @@ -440,24 +445,28 @@ export const onSignature: OnSignatureHandler = async ({ }) => { const insights = /* Get insights */; return { - content: panel([ - heading("My Signature Insights"), - text("Here are the insights:"), - ...(insights.map((insight) => text(insight.value))), - ]), + content: ( + + My Signature Insights + Here are the insights: + {insights.map((insight) => ( + {insight.value} + ))} + + ), severity: SeverityLevel.Critical, }; }; ``` - + -```js title="index.js" -import { SeverityLevel } from "@metamask/snaps-sdk"; +```typescript title="index.ts" +import type { OnSignatureHandler, SeverityLevel } from "@metamask/snaps-sdk"; import { panel, heading, text } from "@metamask/snaps-sdk"; -module.exports.onSignature = async ({ +export const onSignature: OnSignatureHandler = async ({ signature, signatureOrigin, }) => { @@ -476,6 +485,7 @@ module.exports.onSignature = async ({ + ## `onTransaction` To provide [transaction insights](../features/transaction-insights.md) before a user signs a @@ -512,11 +522,11 @@ An object containing: #### Example - + -```typescript title="index.ts" +```tsx title="index.tsx" import type { OnTransactionHandler } from "@metamask/snaps-sdk"; -import { panel, heading, text } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; export const onTransaction: OnTransactionHandler = async ({ transaction, @@ -525,22 +535,27 @@ export const onTransaction: OnTransactionHandler = async ({ }) => { const insights = /* Get insights */; return { - content: panel([ - heading("My Transaction Insights"), - text("Here are the insights:"), - ...(insights.map((insight) => text(insight.value))), - ]), + content: ( + + My Transaction Insights + Here are the insights: + {insights.map((insight) => ( + {insight.value} + ))} + + ), }; }; ``` - + -```js title="index.js" +```typescript title="index.ts" +import type { OnTransactionHandler } from "@metamask/snaps-sdk"; import { panel, heading, text } from "@metamask/snaps-sdk"; -module.exports.onTransaction = async ({ +export const onTransaction: OnTransactionHandler = async ({ transaction, chainId, transactionOrigin, @@ -577,34 +592,37 @@ None. #### Example - + -```typescript title="index.ts" -import type { OnUpdateHandler } from "@metamask/snaps-sdk" -import { heading, panel, text } from "@metamask/snaps-sdk" +```tsx title="index.tsx" +import type { OnUpdateHandler } from "@metamask/snaps-sdk"; +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; export const onUpdate: OnUpdateHandler = async () => { await snap.request({ method: "snap_dialog", params: { type: "alert", - content: panel([ - heading("Thank you for updating my Snap"), - text("New features added in this version:"), - text("Added a dialog that appears when updating."), - ]), + content: ( + + Thank you for updating my Snap + New features added in this version: + Added a dialog that appears when updating. + + ), }, }) } ``` - + -```js title="index.js" -import { heading, panel, text } from "@metamask/snaps-sdk" +```typescript title="index.ts" +import type { OnUpdateHandler } from "@metamask/snaps-sdk"; +import { heading, panel, text } from "@metamask/snaps-sdk"; -module.exports.onUpdate = async () => { +export const onUpdate: OnUpdateHandler = async () => { await snap.request({ method: "snap_dialog", params: { diff --git a/snaps/reference/snaps-api.md b/snaps/reference/snaps-api.md index e6cfa2f55ea..d85837deee4 100644 --- a/snaps/reference/snaps-api.md +++ b/snaps/reference/snaps-api.md @@ -48,8 +48,34 @@ Return value depends on the dialog `type`: #### Example -```javascript title="index.js" -import { panel, text, heading } from "@metamask/snaps-sdk" + + + +```tsx title="index.tsx" +import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx"; + +const walletAddress = await snap.request({ + method: "snap_dialog", + params: { + type: "prompt", + content: ( + + What is the wallet address? + Please enter the wallet address to be monitored + + ), + placeholder: "0x123...", + }, +}); + +// walletAddress will be a string containing the address entered by the user. +``` + + + + +```typescript title="index.ts" +import { panel, text, heading } from "@metamask/snaps-sdk"; const walletAddress = await snap.request({ method: "snap_dialog", @@ -66,6 +92,9 @@ const walletAddress = await snap.request({ // walletAddress will be a string containing the address entered by the user. ``` + + + ## `snap_getBip32Entropy` :::danger important @@ -465,8 +494,37 @@ The user's locale setting as a [language code](https://github.com/MetaMask/metam #### Example -```javascript title="index.js" -import { panel, text } from "@metamask/snaps-sdk" + + + +```tsx title="index.tsx" +import { Box, Text } from "@metamask/snaps-sdk/jsx"; + +const locale = await snap.request({ method: "snap_getLocale" }); + +let greeting = "Hello"; +if(locale === "es") { + greeting = "Hola"; +} + +await snap.request({ + method: "snap_dialog", + params: { + type: "alert", + content: ( + + {greeting} + + ), + }, +}); +``` + + + + +```typescript title="index.ts" +import { panel, text } from "@metamask/snaps-sdk"; const locale = await snap.request({ method: "snap_getLocale" }) @@ -484,6 +542,9 @@ await snap.request({ }) ``` + + + ## `snap_manageAccounts` Manages [account management Snap](../features/custom-evm-accounts/index.md) accounts. diff --git a/src/theme/Tabs/index.tsx b/src/theme/Tabs/index.tsx new file mode 100644 index 00000000000..d519f2d320c --- /dev/null +++ b/src/theme/Tabs/index.tsx @@ -0,0 +1,177 @@ +import React, {cloneElement, type ReactElement} from 'react'; +import clsx from 'clsx'; +import { + useScrollPositionBlocker, + useTabs, + sanitizeTabsChildren, + type TabItemProps, +} from '@docusaurus/theme-common/internal'; +import useIsBrowser from '@docusaurus/useIsBrowser'; +import type {Props} from '@theme/Tabs'; +import styles from './styles.module.css'; + +function TabList({ + className, + block, + selectedValue, + selectValue, + tabValues, +}: Props & ReturnType) { + console.log(tabValues); + const tabRefs: (HTMLLIElement | null)[] = []; + const {blockElementScrollPositionUntilNextRender} = + useScrollPositionBlocker(); + + const handleTabChange = ( + event: + | React.FocusEvent + | React.MouseEvent + | React.KeyboardEvent, + ) => { + const newTab = event.currentTarget; + const newTabIndex = tabRefs.indexOf(newTab); + const newTabValue = tabValues[newTabIndex]!.value; + + if (newTabValue !== selectedValue) { + blockElementScrollPositionUntilNextRender(newTab); + selectValue(newTabValue); + } + }; + + const handleKeydown = (event: React.KeyboardEvent) => { + let focusElement: HTMLLIElement | null = null; + + switch (event.key) { + case 'Enter': { + handleTabChange(event); + break; + } + case 'ArrowRight': { + const nextTab = tabRefs.indexOf(event.currentTarget) + 1; + focusElement = tabRefs[nextTab] ?? tabRefs[0]!; + break; + } + case 'ArrowLeft': { + const prevTab = tabRefs.indexOf(event.currentTarget) - 1; + focusElement = tabRefs[prevTab] ?? tabRefs[tabRefs.length - 1]!; + break; + } + default: + break; + } + + focusElement?.focus(); + }; + + return ( +
    + {tabValues.map(({value, label, attributes}) => ( +
  • tabRefs.push(tabControl)} + onKeyDown={handleKeydown} + onClick={handleTabChange} + {...attributes} + className={clsx( + 'tabs__item', + styles.tabItem, + attributes?.className as string, + { + 'tabs__item--active': selectedValue === value, + }, + )}> + {label ?? value} +
  • + ))} +
+ ); +} + +function TabContent({ + lazy, + children, + selectedValue, +}: Props & ReturnType) { + const childTabs = (Array.isArray(children) ? children : [children]).filter( + Boolean, + ) as ReactElement[]; + if (lazy) { + const selectedTabItem = childTabs.find( + (tabItem) => tabItem.props.value === selectedValue, + ); + if (!selectedTabItem) { + // fail-safe or fail-fast? not sure what's best here + return null; + } + return cloneElement(selectedTabItem, {className: 'margin-top--md'}); + } + return ( +
+ {childTabs.map((tabItem, i) => + cloneElement(tabItem, { + key: i, + hidden: tabItem.props.value !== selectedValue, + }), + )} +
+ ); +} + +function TabsComponent(props: Props): JSX.Element { + const tabs = useTabs(props); + /** + * Tabs and TabItems aren't rendered in a straightforward manner. + * Swizzling the TabItem component has no effect: the TabItem is used only for its data. + * The actual tab items are rendered by the TabList component above. + * Here we are adding custom styling to a tab by combining the tab values from the hook + * with the props of the TabItem. + */ + const finalTabs = { + ...tabs, + tabValues: tabs.tabValues.map((tabValue, idx) => ({ + ...tabValue, + attributes: { + ...tabValue.attributes, + className: clsx( + tabValue.attributes?.className, + { + [styles.flaskOnly]: props.children[idx]?.props.flaskOnly, + [styles.deprecated]: props.children[idx]?.props.deprecated + } + ) + } + })) + } + return ( +
+ + +
+ ); +} + +export default function Tabs(props: Props): JSX.Element { + const isBrowser = useIsBrowser(); + return ( + + {sanitizeTabsChildren(props.children)} + + ); +} diff --git a/src/theme/Tabs/styles.module.css b/src/theme/Tabs/styles.module.css new file mode 100644 index 00000000000..0638d111d6b --- /dev/null +++ b/src/theme/Tabs/styles.module.css @@ -0,0 +1,27 @@ +.tabList { + margin-bottom: var(--ifm-leading); +} + +.tabItem { + margin-top: 0 !important; +} + +.flaskOnly::after { + content: "Flask"; + margin-left: 0.5em; + background-color: var(--mm-flask-background-color); + color: var(--mm-flask-color); + border-radius: 4px; + padding: 2px 4px; + font-size: 0.8em; +} + +.deprecated::after { + content: "deprecated"; + margin-left: 0.5em; + background-color: var(--ifm-color-warning-contrast-background); + color: var(--ifm-color-warning-contrast-foreground); + border-radius: 4px; + padding: 2px 4px; + font-size: 0.8em; +} \ No newline at end of file diff --git a/wallet/how-to/use-sdk/gaming/unity/index.md b/wallet/how-to/use-sdk/gaming/unity/index.md index 105ab1b2774..687a83fbdd0 100644 --- a/wallet/how-to/use-sdk/gaming/unity/index.md +++ b/wallet/how-to/use-sdk/gaming/unity/index.md @@ -6,7 +6,7 @@ tags: - Unity SDK --- -import YoutubeEmbed from '@site/src/components/YoutubeEmbed'; +import YoutubeEmbed from "@site/src/components/YoutubeEmbed"; # Use MetaMask SDK with Unity