> ## 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.

# App Bridge (embedded UI)

> How your app runs inside the Salesive dashboard, the background pre-load lifecycle, runtime permissions, and the salesive-dev-tools/permissions package.

A Salesive app can ship an embedded UI that opens inside the merchant's dashboard — like a
panel or full-screen sheet — alongside its backend API access. This page documents how that
works: the iframe environment, the background pre-load system, lifecycle events, and the
runtime permissions the merchant can grant at any time.

## How the embedded UI works

When a merchant opens your app from the dashboard dock, your app's `appUrl` is loaded in a
sandboxed `<iframe>` inside the dashboard. The dashboard appends a few context parameters so
your app can recognise the active store and user without a separate auth step:

```
https://your-app.com/embed
  ?shop=<store-id>
  &name=<store-name>
  &user=<salesive-user-id>
  &embedded=1
  &host=https://app.salesive.com
```

| Parameter  | Description                                                                                                                                                                                                                       |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `shop`     | The installed store's id. Use this to look up your stored OAuth tokens and make API calls on behalf of this store.                                                                                                                |
| `name`     | The store's display name. Useful for greeting the merchant before your API call returns.                                                                                                                                          |
| `user`     | The Salesive user id of the merchant currently using the dashboard. Check this against your session to detect account switches (see [session binding](/apps/oauth-install#step-3b--bind-your-session-to-the-authenticated-user)). |
| `embedded` | Always `"1"`. Use this to detect the embedded context and hide your public marketing shell.                                                                                                                                       |
| `host`     | The dashboard's origin (`https://app.salesive.com`).                                                                                                                                                                              |

<Note>
  These parameters are for **convenience** — they identify the context, not prove identity.
  The actual authentication proof is your signed, HttpOnly session cookie set during the OAuth
  install (see [Install flow](/apps/oauth-install)). Never trust `shop`/`user` from the URL
  without validating against your session.
</Note>

## Background pre-loading

The dashboard **pre-loads every installed app in a hidden iframe** when the merchant opens
the dashboard — before the merchant ever taps the dock icon. This means:

* Your app's code, assets, and connections are already warm by the time the merchant opens it.
* Background work (polling, WebSocket connections, sync) can start immediately, not only
  when the merchant looks at the app.
* The first open is **instant** — no loading spinner if your app has initialised in the
  background.

When your app's iframe loads in the background, the dashboard sends a `salesive:app-wake`
postMessage. Register a handler for this to start background work:

```js theme={null}
import { onAppWake } from "salesive-dev-tools/permissions";

onAppWake(() => {
  connectWebSocket();
  startOrderPolling();
  prefetchCriticalData();
});
```

The dashboard pre-loads background iframes with real dimensions (not 0×0) and off-screen
positioning so browsers do **not** throttle their JavaScript execution. Your timers,
WebSocket connections, and `fetch()` calls all work normally in the background.

## Full lifecycle

Every embedded app goes through these events:

```
dashboard opens
      │
      ▼
 salesive:app-wake          ← iframe loaded in background; start sync, sockets, etc.
      │
      │  (merchant taps dock icon)
      ▼
 salesive:app-opened        ← app is now visible; show fresh UI, stop any "background only" modes
      │
      │  (merchant presses − or swipes down)
      ▼
 salesive:app-minimized     ← app hidden but still running; pause heavy rendering work
      │
      │  (merchant reopens)
      ▼
 salesive:app-opened        ← visible again
      │
      │  (merchant presses × / close)
      ▼
 salesive:app-closed        ← dashboard has told the app it was dismissed; save unsaved state
                               (app is still running in background — wake/open may follow)
```

The app keeps running through every state change. Only an uninstall removes the iframe.

## Runtime permissions

Some capabilities — expanding the app programmatically, reading the clipboard, cancelling
orders — require explicit, time-limited merchant approval. Unlike OAuth install scopes (which
are approved once at install), runtime permissions can be requested at any time via a
dashboard modal the merchant approves or denies.

The merchant chooses a duration (once, 1 day, 7 days, etc.) and can revoke at any time.

### Available runtime permissions

| Permission            | What it lets your app do                                                                   |
| --------------------- | ------------------------------------------------------------------------------------------ |
| `AUTO_LAUNCH`         | Call `onLaunch()` to expand the app from background without the merchant tapping the dock. |
| `WRITE_DOMAINS`       | Add and remove custom domains linked to the merchant's store.                              |
| `CLIPBOARD_READ`      | Read text from the merchant's system clipboard.                                            |
| `WRITE_ORDERS_CANCEL` | Cancel orders on the merchant's behalf (irreversible).                                     |

<Warning>
  Runtime permissions are separate from OAuth scopes. OAuth scopes gate the **backend API**.
  Runtime permissions gate **dashboard-side capabilities** (UI expansion, clipboard, etc.).
  Your app needs both: the right scope to call an API endpoint, and the right runtime
  permission for any dashboard behaviour beyond rendering inside the iframe.
</Warning>

## The App Bridge package

Install the helper:

```bash theme={null}
npm install salesive-dev-tools
```

Import from the permissions subpath for the smallest bundle (tree-shakes the CLI and Vite
plugin out):

```js theme={null}
import {
  PERMISSIONS,
  DURATIONS,
  APP_EVENTS,
  requestPermission,
  getGrantedPermissions,
  onLaunch,
  onAppWake,
  onAppOpened,
  onAppMinimized,
  onAppClosed,
  onEvent,
} from "salesive-dev-tools/permissions";
```

Or from the main package (same exports):

```js theme={null}
import { requestPermission, onAppWake } from "salesive-dev-tools";
```

TypeScript types are bundled — no `@types/` package needed.

***

### `onAppWake(callback)`

Fires when the dashboard first loads the app in the background. This is the earliest point
at which your app's JavaScript runs. Use it to start long-running background work.

```ts theme={null}
function onAppWake(callback: () => void): () => void  // returns unsubscribe
```

```js theme={null}
import { onAppWake } from "salesive-dev-tools/permissions";

onAppWake(async () => {
  await connectWebSocket();
  const grants = await getGrantedPermissions();
  if (PERMISSIONS.AUTO_LAUNCH in grants) scheduleAutoLaunchTimer();
});
```

***

### `onAppOpened(callback)`

Fires each time the merchant expands the app to full view (first open or restore from
minimized). Use it to refresh UI data that may be stale after time in the background.

```ts theme={null}
function onAppOpened(callback: () => void): () => void
```

```js theme={null}
onAppOpened(() => {
  refreshDashboard();
  clearBadge();
});
```

***

### `onAppMinimized(callback)`

Fires when the merchant presses − (minimize) or swipes the sheet down on mobile. The app
is hidden but still running.

```ts theme={null}
function onAppMinimized(callback: () => void): () => void
```

```js theme={null}
onAppMinimized(() => {
  pauseHeavyAnimations();
  reducePollingFrequency();
});
```

***

### `onAppClosed(callback)`

Fires when the merchant presses × (close / dismiss). The app is still running in the
background — this is the same as minimized from a lifecycle standpoint, but signals that
the merchant deliberately dismissed the panel rather than just hiding it.

```ts theme={null}
function onAppClosed(callback: () => void): () => void
```

```js theme={null}
onAppClosed(() => {
  saveDraft();
  cancelPendingAnimations();
});
```

***

### `requestPermission(permission, options?, timeoutMs?)`

Show the merchant a permission modal. Returns `true` if approved, `false` if denied or
timed out. The modal has a **10-second auto-deny countdown**.

```ts theme={null}
function requestPermission(
  permission : Permission,
  options?   : DurationOption[],  // duration choices; omit = all five. First = default.
  timeoutMs? : number,            // default 30 000 ms
): Promise<boolean>
```

```js theme={null}
import { PERMISSIONS, DURATIONS, requestPermission } from "salesive-dev-tools/permissions";

// Check existing grant first — no modal shown for this call:
const grants = await getGrantedPermissions();

if (!(PERMISSIONS.AUTO_LAUNCH in grants)) {
  // Request — show 7-day and 31-day options:
  const granted = await requestPermission(
    PERMISSIONS.AUTO_LAUNCH,
    [DURATIONS.SEVEN_DAYS, DURATIONS.THIRTY_ONE_DAYS]
  );
  if (!granted) return; // merchant denied
}
```

**Duration options**

| Constant                    | String     | Lasts                      |
| --------------------------- | ---------- | -------------------------- |
| `DURATIONS.ONCE`            | `"once"`   | Consumed on first exercise |
| `DURATIONS.ONE_DAY`         | `"1day"`   | 1 day                      |
| `DURATIONS.SEVEN_DAYS`      | `"7days"`  | 7 days                     |
| `DURATIONS.FOURTEEN_DAYS`   | `"14days"` | 14 days                    |
| `DURATIONS.THIRTY_ONE_DAYS` | `"31days"` | 31 days                    |

***

### `getGrantedPermissions(timeoutMs?)`

Silently retrieve the merchant's active grants — **no modal shown**. Call this on wake or
mount to avoid requesting a permission the merchant already approved.

```ts theme={null}
function getGrantedPermissions(timeoutMs?: number): Promise<PermissionGrantMap>
```

Returns a map of `Permission → GrantRecord`:

```ts theme={null}
interface GrantRecord {
  mode      : "once" | "1day" | "7days" | "14days" | "31days";
  grantedAt : string;           // ISO-8601 timestamp
  expiresAt : string | null;    // null for "once" grants
  usedAt    : string | null;    // set after a "once" grant is consumed
}
```

```js theme={null}
onAppWake(async () => {
  const grants = await getGrantedPermissions();

  if (PERMISSIONS.AUTO_LAUNCH in grants) {
    // Merchant already approved — set up the timer directly
    startAutoLaunchTimer(grants[PERMISSIONS.AUTO_LAUNCH].expiresAt);
  }
});
```

***

### `onLaunch()`

Ask the dashboard to expand (un-minimize) this app. The call is silently ignored if
`AUTO_LAUNCH` has not been granted.

```js theme={null}
// Auto-reopen after 30 s when minimized and AUTO_LAUNCH is granted:
onAppMinimized(() => {
  const timer = setTimeout(onLaunch, 30_000);
  const unsub = onAppOpened(() => { clearTimeout(timer); unsub(); });
});
```

***

### `onEvent(type, callback)`

Subscribe to any postMessage event by type string — including future event types not yet in
this helper. Callbacks receive the full raw message data object.

```ts theme={null}
function onEvent(
  type     : string,
  callback : (data: Record<string, unknown>) => void,
): () => void
```

```js theme={null}
import { APP_EVENTS, onEvent } from "salesive-dev-tools/permissions";

// Forward all lifecycle events to your analytics:
for (const type of Object.values(APP_EVENTS)) {
  onEvent(type, (data) => analytics.track(data.type, data));
}
```

**`APP_EVENTS` constants**

| Constant                         | Event type string              | When it fires                         |
| -------------------------------- | ------------------------------ | ------------------------------------- |
| `APP_EVENTS.WAKE`                | `salesive:app-wake`            | Background iframe first loaded        |
| `APP_EVENTS.OPENED`              | `salesive:app-opened`          | App expanded to full view             |
| `APP_EVENTS.MINIMIZED`           | `salesive:app-minimized`       | Merchant pressed −                    |
| `APP_EVENTS.CLOSED`              | `salesive:app-closed`          | Merchant pressed ×                    |
| `APP_EVENTS.PERMISSION_RESPONSE` | `salesive:permission-response` | Response to `requestPermission()`     |
| `APP_EVENTS.PERMISSIONS_STATE`   | `salesive:permissions-state`   | Response to `getGrantedPermissions()` |

***

## Complete React example

```jsx theme={null}
import { useEffect, useState } from "react";
import {
  PERMISSIONS, DURATIONS, APP_EVENTS,
  requestPermission, getGrantedPermissions,
  onLaunch, onAppWake, onAppOpened, onAppMinimized, onAppClosed,
} from "salesive-dev-tools/permissions";

export default function App() {
  const [autoLaunchGranted, setAutoLaunchGranted] = useState(false);

  // Background init — fires before merchant opens the app
  useEffect(() => {
    return onAppWake(async () => {
      // Start background work immediately
      connectWebSocket();

      // Check existing grants silently
      const grants = await getGrantedPermissions();
      setAutoLaunchGranted(PERMISSIONS.AUTO_LAUNCH in grants);
    });
  }, []);

  // Foreground lifecycle
  useEffect(() => {
    const unOpen = onAppOpened(() => refreshDashboard());
    const unMin  = onAppMinimized(() => pauseHeavyWork());
    const unCls  = onAppClosed(() => saveDraft());
    return () => { unOpen(); unMin(); unCls(); };
  }, []);

  // Auto-reopen when minimized (only if granted)
  useEffect(() => {
    if (!autoLaunchGranted) return;
    return onAppMinimized(() => {
      const t = setTimeout(onLaunch, 30_000);
      const u = onAppOpened(() => { clearTimeout(t); u(); });
    });
  }, [autoLaunchGranted]);

  async function handleRequestAutoLaunch() {
    const ok = await requestPermission(
      PERMISSIONS.AUTO_LAUNCH,
      [DURATIONS.SEVEN_DAYS, DURATIONS.THIRTY_ONE_DAYS]
    );
    setAutoLaunchGranted(ok);
  }

  return (
    <div>
      {autoLaunchGranted
        ? <p>✓ Auto-launch active — app will reopen when minimized.</p>
        : <button onClick={handleRequestAutoLaunch}>Enable auto-reopen</button>
      }
    </div>
  );
}
```

***

## Environment notes

* **Browser only** — all functions are no-ops in Node.js / SSR contexts (return `false`,
  `{}`, or a no-op unsubscribe). Safe to import in Next.js or Remix server components.
* **No runtime dependencies** — the permissions subpath has zero dependencies.
* **Single listener** — one `window.addEventListener("message", …)` is registered on module
  import. Multiple concurrent `requestPermission()` calls resolve independently.

## Next steps

<CardGroup cols={2}>
  <Card title="OAuth install flow" icon="arrow-right-to-bracket" href="/apps/oauth-install">
    Set up the token exchange and session binding.
  </Card>

  <Card title="Scopes & permissions" icon="key" href="/apps/scopes-permissions">
    The API scopes your backend calls need.
  </Card>

  <Card title="Build & publish" icon="rocket" href="/apps/building-publishing">
    Submit your app for marketplace review.
  </Card>

  <Card title="Webhooks" icon="bell" href="/apps-api/webhooks">
    Real-time store events for your backend.
  </Card>
</CardGroup>
