> ## Documentation Index
> Fetch the complete documentation index at: https://docs.salesive.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dashboard UI extensions

> Add actions and blocks from your app to the order, catalog item and customer pages of the Salesive dashboard.

Besides opening full screen from the app launcher, your app can appear right where the
merchant works: on an order, a catalog item (product, food, service or digital product) or
a customer.

* An **action** is an item in the page's **Apps** menu. Choosing it opens your app in a
  modal, already told which resource the merchant was looking at. Use it for one-off tasks
  such as "Print packing slip" or "Send to courier".
* A **block** is a small card inside the page. Use it for at-a-glance information such as a
  fraud score, a tracking status or a loyalty balance.

Both are iframes of pages in your own app, talking to the dashboard over the
[App Bridge](https://docs.salesive.com/apps/app-bridge.md).

## Targets

| Target | Where it shows | Shows only when the store granted |
| - | - | - |
| `order.details.action` | Apps menu on the order details page | `READ_ORDERS` |
| `order.details.block` | Card on the order details page | `READ_ORDERS` |
| `product.details.action` | Apps menu on the product, food, service and digital product pages | `READ_INVENTORY` |
| `product.details.block` | Card on those same pages | `READ_INVENTORY` |
| `customer.details.action` | Apps menu in the customer details window | `READ_CUSTOMERS` |
| `customer.details.block` | Card in the customer details window | `READ_CUSTOMERS` |

The `product.*` targets cover every catalog type. The context tells you which one the store
uses: `resource.type` is `product` (ecommerce), `food` (restaurant), `service` (business)
or `digital-product` (digital). Fetch the item from the matching API (`/products`, `/foods`,
`/services` or `/digital-products`).

Declaring a target grants nothing. An extension only appears on stores whose installation of
your app holds the scope in the last column, and it is hidden again the moment the app is
uninstalled.

## Declaring extensions

In the dashboard go to **Apps**, **Developer**, open your app and find **Dashboard
extensions**. Add one row per extension:

| Field | Rules |
| - | - |
| Shows on | One of the six targets above. |
| Label | 1 to 40 characters. The menu item text for an action, the card title for a block. Labels must be unique per target. |
| Path in your app | Must start with `/`, for example `/extensions/order-slip?compact=1`. No scheme, no host, no `//`, no backslashes or spaces. |
| Height | Optional, in pixels. Blocks 48 to 600 (default 160), actions 120 to 720 (default 480). Your app can change it at runtime. |

An app can declare up to 12 extensions. The API refuses an invalid row with a message that
says which row and why.

Through the API, send the list as `dashboardExtensions` with `PUT /api/v1/apps/developer/:id`:

```json theme={null}
{
  "dashboardExtensions": [
    { "target": "order.details.action", "label": "Print packing slip", "path": "/extensions/slip", "height": 480 },
    { "target": "order.details.block", "label": "Fraud score", "path": "/extensions/fraud" }
  ]
}
```

This is a different field from `extensions`, which holds your
[storefront extensions](https://docs.salesive.com/apps/storefront-extensions.md) (embeds and blocks on the shop). Send
`null` or `[]` to remove every dashboard extension.

### Paths always stay on your origin

You declare a path, never a URL. The dashboard resolves it against the **install / launch
URL** of the store's installation, so a development install loads your development app URL
and a production install loads your production one. The dashboard checks the
resolved URL has the same origin as that app URL before it loads anything.

## What the frame receives

The frame URL carries the same parameters as the full-screen app, plus three more:

| Parameter | Value |
| - | - |
| `shop`, `name`, `user`, `embedded`, `host` | As for the [embedded app](https://docs.salesive.com/apps/app-bridge.md#how-the-embedded-ui-works). |
| `extension` | The target, for example `order.details.block`. |
| `resourceType` | `order`, `product`, `food`, `service`, `digital-product` or `customer`. |
| `resourceId` | The resource's id. |

When the frame loads, the dashboard also sends a `salesive:extension-context` message:

```json theme={null}
{
  "type": "salesive:extension-context",
  "target": "order.details.action",
  "kind": "action",
  "resource": { "type": "order", "id": "6650f1c2a8b4e0d1c2f3a4b5" },
  "shopId": "664f0a...",
  "installationId": "6651aa...",
  "label": "Print packing slip"
}
```

> [!WARNING]
>
> The resource id is a pointer, not a permission. Always load the resource through the API with
> the access token you hold for that store, and check the `shop` matches the store you expect.
> Never trust the id alone to decide what a merchant may see.

## Using the bridge

```js theme={null}
import {
  isExtension,
  getExtensionContext,
  autoResizeExtension,
  closeExtension,
} from "salesive-dev-tools/permissions";

if (isExtension()) {
  const ctx = await getExtensionContext();
  const order = await myApi.getOrder(ctx.shopId, ctx.resource.id);

  if (ctx.kind === "block") autoResizeExtension();
}

// In an action, once the merchant is done:
closeExtension({ reload: true });
```

| Function | What it does |
| - | - |
| `isExtension()` | `true` when the page runs as an extension (the URL has `extension=`). |
| `getExtensionContext(timeoutMs?)` | Resolves the context above, asking the dashboard if it has not arrived yet. Resolves `null` outside an extension or after `timeoutMs` (default 5000). |
| `onExtensionContext(callback)` | Calls `callback` now if the context is known, and on every update. Returns an unsubscribe function. |
| `resizeExtension(height)` | Sets the frame height in pixels (`salesive:extension-resize`). Clamped to the limits in the table above. |
| `autoResizeExtension()` | Keeps the frame as tall as your content with a `ResizeObserver`. Returns a stop function. |
| `closeExtension({ reload })` | Closes an action's modal, or hides a block until the page is opened again (`salesive:extension-close`). `reload: true` makes the page refetch the resource, so changes your app made show at once. |

### React example

```jsx theme={null}
import { useEffect, useState } from "react";
import {
  getExtensionContext,
  autoResizeExtension,
  closeExtension,
} from "salesive-dev-tools/permissions";

export default function OrderSlipExtension() {
  const [order, setOrder] = useState(null);

  useEffect(() => {
    let stop = () => {};
    getExtensionContext().then(async (ctx) => {
      if (!ctx) return;
      setOrder(await fetchOrder(ctx.shopId, ctx.resource.id));
      stop = autoResizeExtension();
    });
    return () => stop();
  }, []);

  if (!order) return <p>Loading...</p>;
  return (
    <div>
      <p>Order #{order.orderId}</p>
      <button
        onClick={async () => {
          await printSlip(order);
          closeExtension({ reload: false });
        }}
      >
        Print slip
      </button>
    </div>
  );
}
```

## Limits and behaviour

* Extension frames use a stricter sandbox than the full-screen app: scripts, forms, your own
  cookies and storage, popups and downloads are allowed; navigating the dashboard window is
  not.
* Only the extension messages above are answered inside an extension frame. Other bridge
  calls such as `pickFiles()` or `requestPermission()` work only in the full-screen app.
* Blocks load lazily, when the merchant scrolls near them. Pages without any extension from
  an installed app load no extension code at all.
* The menu and blocks list extensions in the order the apps were installed (newest first),
  then in the order you declared them.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.