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

# Actions Reference

> Complete reference for all App Bridge actions

# App Bridge Actions Reference

The App Bridge SDK exposes a class for each action family plus the raw
`dispatch` / `dispatchAndWait` calls underneath. Use the classes when you can
— they validate types, manage subscriptions, and handle cleanup. Drop to the
raw API when you need an action the SDK hasn't wrapped yet.

## Calling Convention

The SDK signatures are:

```ts theme={null}
app.dispatch(action: string, payload?: object): string
app.dispatchAndWait(action: string, payload?: object): Promise<object>
app.subscribe(action: string, callback: (payload, error?) => void): () => void
```

`action` is the **string** name (e.g. `'TOAST_SHOW'`), not an object. The
SDK wraps the payload into the wire format
`{ type: 'APP_BRIDGE_ACTION', action, payload, id }` and posts it to the
host. The host responds with `{ type: 'APP_BRIDGE_RESPONSE', action, id,
payload, error? }`.

```js theme={null}
import { createApp, Toast, Modal, ResourcePicker } from '@launchmystore/app-bridge';

const app = createApp({
  apiKey: 'your-client-id',
  host: new URLSearchParams(location.search).get('host'),
});

// Class-based — preferred
Toast.success(app, 'Saved!');

// Raw — equivalent
app.dispatch('TOAST_SHOW', { message: 'Saved!', type: 'success' });

// Request/response — class (subscribe pattern; dispatch() returns void)
ResourcePicker.create(app, { resourceType: 'product' })
  .subscribe('select', ({ selection }) => console.log(selection))
  .dispatch();

// Request/response — raw (resolves with { pickerId, selection })
const result = await app.dispatchAndWait('RESOURCE_PICKER_OPEN', { resourceType: 'product' });
```

## Action Catalogue

| Family                        | Class               | Action names                                                                                                            |
| ----------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Toast                         | `Toast`             | `TOAST_SHOW`, `TOAST_DISMISS`                                                                                           |
| Modal                         | `Modal`             | `MODAL_OPEN`, `MODAL_CLOSE`                                                                                             |
| Resource Picker               | `ResourcePicker`    | `RESOURCE_PICKER_OPEN`, `RESOURCE_PICKER_CLOSE`                                                                         |
| Title Bar                     | `TitleBar`          | `TITLE_BAR_UPDATE`                                                                                                      |
| Navigation Menu               | `NavigationMenu`    | `NAV_MENU_UPDATE`                                                                                                       |
| Contextual Save Bar           | `ContextualSaveBar` | `CONTEXTUAL_SAVE_BAR`                                                                                                   |
| Redirect                      | `Redirect`          | `REDIRECT`                                                                                                              |
| Loading                       | (raw)               | `LOADING_START`, `LOADING_STOP`                                                                                         |
| Fullscreen                    | `Fullscreen`        | `FULLSCREEN_ENTER`, `FULLSCREEN_EXIT`, `FULLSCREEN_TOGGLE` (host posts back `FULLSCREEN_ENTERED` / `FULLSCREEN_EXITED`) |
| Leave Confirmation            | `LeaveConfirmation` | `LEAVE_CONFIRMATION_ENABLE`, `LEAVE_CONFIRMATION_DISABLE`                                                               |
| Session Token                 | `SessionToken`      | `SESSION_TOKEN_REQUEST`                                                                                                 |
| User                          | `User`              | `USER_FETCH`                                                                                                            |
| Config                        | `Config`            | `CONFIG_FETCH`                                                                                                          |
| Environment                   | `Environment`       | `ENVIRONMENT_FETCH`                                                                                                     |
| Features                      | `Features`          | `FEATURES_QUERY`                                                                                                        |
| Clipboard                     | `Clipboard`         | `CLIPBOARD_READ_REQUEST` (writes run iframe-side, no postMessage)                                                       |
| Scanner                       | `Scanner`           | `SCANNER_OPEN`, `SCANNER_CLOSE`                                                                                         |
| History                       | `History`           | `HISTORY_PUSH`, `HISTORY_REPLACE`, `HISTORY_BACK`, `HISTORY_FORWARD`, `HISTORY_GO`                                      |
| Print                         | `Print`             | `PRINT`                                                                                                                 |
| Share                         | `Share`             | `SHARE`                                                                                                                 |
| Lifecycle                     | `Lifecycle`         | host-pushed `APP_FOCUS`, `APP_BLUR`, `APP_VISIBLE`, `APP_HIDDEN`                                                        |
| Cart (checkout/post-purchase) | `Cart`              | `CART_LINES_CHANGE`, `DISCOUNT_CODE_CHANGE`, `GIFT_CARD_CHANGE`, `NOTE_CHANGE`, `ATTRIBUTE_CHANGE`                      |
| Buyer Journey (checkout)      | `BuyerJourney`      | `BUYER_JOURNEY_INTERCEPT_REQUEST`, `BUYER_JOURNEY_INTERCEPT_RESPONSE`                                                   |

<Note>
  Cart and BuyerJourney are checkout-only. They're documented in detail on the
  [Checkout App Bridge](/app-bridge/checkout) page. Everything else runs in the
  admin host.
</Note>

## UI Actions

### Toast (`TOAST_SHOW`)

```js theme={null}
import { Toast } from '@launchmystore/app-bridge';

// Convenience helpers
Toast.success(app, 'Changes saved!');
Toast.error(app, 'Save failed');
Toast.warning(app, 'Stock low');
Toast.info(app, 'Sync in progress');

// Full control
Toast.create(app, {
  message: 'Saved with tag',
  type: 'success',
  duration: 4000,
  action: { label: 'Undo', onAction: () => undoChange() },
  onDismiss: () => console.log('toast closed'),
}).dispatch();

// Raw equivalent
app.dispatch('TOAST_SHOW', {
  message: 'Saved!',
  type: 'success',
  duration: 4000,
});
```

| Field       | Type                                          | Description                           |
| ----------- | --------------------------------------------- | ------------------------------------- |
| `message`   | string                                        | Up to 200 chars (longer is truncated) |
| `type`      | `'success' \| 'error' \| 'warning' \| 'info'` | Default `info`                        |
| `duration`  | number                                        | ms; default 4000                      |
| `action`    | object                                        | Optional `{ label, onAction }` button |
| `onDismiss` | function                                      | Fired when toast closes               |

### Modal (`MODAL_OPEN` / `MODAL_CLOSE`)

```js theme={null}
import { Modal } from '@launchmystore/app-bridge';

// dispatch() returns void — outcomes arrive via the action callbacks
// and/or the subscribe() events, not a promise.
const modal = Modal.create(app, {
  title: 'Delete Product?',
  message: 'This cannot be undone.',
  primaryAction: { label: 'Delete', onAction: () => deleteProduct() },
  secondaryActions: [{ label: 'Cancel', onAction: () => {} }],
});

modal
  .subscribe('primary',   () => console.log('Delete clicked'))
  .subscribe('secondary', () => console.log('Cancel clicked'))
  .subscribe('close',     () => console.log('dismissed'));

modal.dispatch();   // opens the modal
modal.close();      // closes it programmatically
```

Options: `{ title?, message?, url?, size?: 'small' | 'medium' | 'large' | 'full', primaryAction?: { label, onAction }, secondaryActions?:
Array<{ label, onAction }> }`. Host events (`MODAL_PRIMARY_ACTION`,
`MODAL_SECONDARY_ACTION`, `MODAL_CLOSE`) are matched to the modal by
`modalId` and routed to your callbacks. In React, prefer
[`useConfirmationModal`](/app-bridge/react-hooks#useconfirmationmodal)
for an awaitable confirm dialog.

### Loading (`LOADING_START` / `LOADING_STOP`)

```js theme={null}
app.dispatch('LOADING_START');
await save();
app.dispatch('LOADING_STOP');
```

### Fullscreen (`FULLSCREEN_ENTER` / `EXIT` / `TOGGLE`)

```js theme={null}
import { Fullscreen, FullscreenAction } from '@launchmystore/app-bridge';

const fs = Fullscreen.create(app);   // create(app) takes only the app
fs.enter();                          // or fs.exit() / fs.toggle()
fs.dispatch(FullscreenAction.ENTER); // enum form: ENTER | EXIT
// or raw
app.dispatch('FULLSCREEN_ENTER');

// The host posts back FULLSCREEN_ENTERED / FULLSCREEN_EXITED —
// subscribe for state mirroring:
fs.subscribe('enter', () => setIsFullscreen(true))
  .subscribe('exit',  () => setIsFullscreen(false));
```

## Title Bar (`TITLE_BAR_UPDATE`)

```js theme={null}
import { TitleBar } from '@launchmystore/app-bridge';

// Buttons use `label` + an `onAction` FUNCTION — the class routes the
// host's TITLE_BAR_PRIMARY_ACTION / TITLE_BAR_SECONDARY_ACTION events
// to your callbacks; you don't subscribe to custom event names.
TitleBar.create(app, {
  title: 'Product Reviews',
  primaryAction: { label: 'Add Review', onAction: () => addReview() },
  secondaryActions: [
    { label: 'Export',   onAction: () => exportAll() },
    { label: 'Settings', onAction: () => openSettings() },
  ],
  breadcrumbs: [
    { label: 'Apps',   url: '/admin/apps' },
    { label: 'My App', url: '/admin/apps/my-app' },
  ],
}).dispatch();
```

## Navigation

### Redirect (`REDIRECT`)

```js theme={null}
import { Redirect } from '@launchmystore/app-bridge';

Redirect.create(app, { url: '/admin/products/123' }).dispatch();
// or shorthand
app.redirect.dispatch({ url: '/admin/products/123' });
// or raw
app.dispatch('REDIRECT', { url: '/admin/products/123' });

// External
app.dispatch('REDIRECT', {
  url: 'https://example.com',
  external: true,
  newTab: true,
});
```

### Navigation Menu (`NAV_MENU_UPDATE`)

```js theme={null}
import { NavigationMenu } from '@launchmystore/app-bridge';

NavigationMenu.create(app, {
  items: [
    { label: 'Dashboard', destination: '/' },
    { label: 'Reviews', destination: '/reviews' },
    { label: 'Settings', destination: '/settings' },
  ],
  active: '/reviews',
}).dispatch();
```

### History (`HISTORY_PUSH` / `REPLACE` / `BACK` / `FORWARD` / `GO`)

```js theme={null}
import { History } from '@launchmystore/app-bridge';

// push/replace take the path as a STRING (optional state object second)
History.create(app).push('/reviews?filter=pending');
History.create(app).replace('/reviews');
History.create(app).back();
History.create(app).forward();
History.create(app).go(-2);
```

## Resource Picker (`RESOURCE_PICKER_OPEN` / `CLOSE`)

```js theme={null}
import { ResourcePicker } from '@launchmystore/app-bridge';

// Class API: dispatch() returns void — results arrive via subscribe().
ResourcePicker.create(app, {
  resourceType: 'product',
  multiple: true,
  selectionIds: ['prod_123', 'prod_987'],   // preferred
  filter: { query: 'shirt' },
})
  .subscribe('select', ({ selection }) => console.log('Selected:', selection))
  .subscribe('cancel', () => console.log('cancelled'))
  .dispatch();

// Raw API: dispatchAndWait resolves with { pickerId, selection }
const result = await app.dispatchAndWait('RESOURCE_PICKER_OPEN', {
  resourceType: 'product',
  multiple: true,
});
if (result.selection.length > 0) {
  console.log('Selected:', result.selection);
}
```

`multiple: true` lets the picker return more than one resource. Pass
`selectionIds` (an array of ids) to pre-select. The legacy
`initialSelectionIds: [{ id }]` shape still works but `selectionIds` is
the recommended shape going forward. On cancel the picker responds with
`{ selection: [] }` — `selection` is always an array and there is **no
`cancelled` field**; an empty selection is the cancel signal (class
consumers get the dedicated `cancel` event).

| Resource Type     | Description                                                                                          |
| ----------------- | ---------------------------------------------------------------------------------------------------- |
| `product`         | Products                                                                                             |
| `product_variant` | Product variants                                                                                     |
| `collection`      | Collections                                                                                          |
| `customer`        | Customers                                                                                            |
| `order`           | Orders                                                                                               |
| `blog`            | Blogs                                                                                                |
| `article`         | Blog articles                                                                                        |
| `page`            | Pages                                                                                                |
| `menu`            | Navigation menus                                                                                     |
| `file`            | Files / media — browse **and upload** (no app scope required; runs under the merchant admin session) |
| `metaobject`      | Metaobjects                                                                                          |

Response payload: `{ pickerId, selection: [...] }` — `selection` is always an
array (length 1 for single-select; empty on cancel).

For **non-file** types each element is a **stable handle string**: `handle` for
product / collection / blog / article / page / menu, `email` for customer,
`order_number` for order, `id` for product\_variant. Store the handle; resolve
extra fields server-side.

For **`file`** each element is a **rich object** so you get a usable URL
directly — no extra lookup needed:

```json theme={null}
{ "id": "<mediaId>", "url": "https://cdn…/asset.png", "filename": "asset.png", "mimeType": "image/png" }
```

<Note>
  The `file` picker is the full **Media Library** — the merchant can search,
  browse, and **upload new files** in-place, then
  pick one or more. `multiple: true` returns several objects. `id` is the stable
  `mediaId`; `url` is a permanent CDN URL. Other resource types select existing
  records only.
</Note>

## Save Bar (`CONTEXTUAL_SAVE_BAR`)

```js theme={null}
import { ContextualSaveBar, SaveBarAction } from '@launchmystore/app-bridge';

const bar = ContextualSaveBar.create(app, {
  saveAction:    { label: 'Save' },
  discardAction: { label: 'Discard' },
  fullWidth: false,
});

// Events come through the bar instance, not app.subscribe():
bar.subscribe('save', async () => {
  bar.setSaveLoading(true);
  await saveChanges();
  bar.hide();
});
bar.subscribe('discard', () => {
  discardChanges();
  bar.hide();
});

bar.show();
// bar.update({ saveAction: { disabled: true } }) while visible
```

The raw dispatch is a single `CONTEXTUAL_SAVE_BAR` action with payload
`{ action: 'SHOW' | 'HIDE' | 'UPDATE', ... }`; the host emits
`CONTEXTUAL_SAVE_BAR_SAVE` / `CONTEXTUAL_SAVE_BAR_DISCARD` events which
the class routes to your `subscribe` callbacks — use the class.

## Leave Confirmation (`LEAVE_CONFIRMATION_ENABLE` / `DISABLE`)

```js theme={null}
import { LeaveConfirmation } from '@launchmystore/app-bridge';

LeaveConfirmation.create(app, {
  message: 'You have unsaved changes. Are you sure you want to leave?',
}).enable();

// later
LeaveConfirmation.create(app).disable();
```

## Session & Auth (`SESSION_TOKEN_REQUEST`)

```js theme={null}
// Simple — but NOT cached: each call issues a fresh SESSION_TOKEN_REQUEST
const token = await app.getSessionToken();

fetch('/api/v1/data', {
  headers: { Authorization: `Bearer ${token}` },
});
```

`app.getSessionToken()` is a thin wrapper over
`dispatchAndWait('SESSION_TOKEN_REQUEST')` — it returns `{ token }` but does
**not** cache. For caching + auto-refresh (reuse the token while more than
30s from its JWT `exp`, refresh otherwise), use the `SessionToken` action
class or the `useSessionToken` React hook.

## User / Config / Environment / Features

```js theme={null}
import { User, Config, Environment, Features } from '@launchmystore/app-bridge';

const user = await User.create(app).fetch();
// { id, email, name, locale, timezone? }

const config = await Config.create(app).fetch();
// { storeId, shopDomain, primaryDomain, hasCustomDomain, shopName,
//   currency, currencySymbol, country, locale, timezone?, plan, features }

const env = await Environment.create(app).fetch();
// { platform, version, apiVersion, embedded, mobile, pos, features }

// Features is a LOCAL capability check — it never round-trips to the host:
const features = Features.create(app);
features.isAvailable('ResourcePicker');       // boolean
features.getAll();                            // string[] of available features
features.checkMultiple(['Toast', 'Share']);   // { Toast: true, Share: false }

// If you want the host's own answer, use the raw action:
const hostFlags = await app.dispatchAndWait('FEATURES_QUERY');
```

Raw action names: `USER_FETCH`, `CONFIG_FETCH`, `ENVIRONMENT_FETCH`,
`FEATURES_QUERY` (raw only — the `Features` class does not dispatch it).

## Browser APIs

### Clipboard (`CLIPBOARD_WRITE` / `CLIPBOARD_READ_REQUEST`)

```js theme={null}
import { Clipboard } from '@launchmystore/app-bridge';

const clipboard = Clipboard.create(app);
await clipboard.write('Copied!');       // or clipboard.copy(text)
const text = await clipboard.read();    // or clipboard.paste()

// One-shot static helper for writes:
await Clipboard.copyText(app, 'Copied!');
```

<Warning>
  Reads go through the host because Chrome blocks `clipboard-read` in
  cross-origin iframes. Writes (`copy()`) work iframe-side directly.
</Warning>

### Scanner (`SCANNER_OPEN` / `SCANNER_CLOSE`)

```js theme={null}
import { Scanner } from '@launchmystore/app-bridge';

try {
  const result = await Scanner.create(app).capture();
  console.log('Scanned:', result.data, result.type); // type: 'qr' | 'barcode'
} catch (err) {
  // Cancel REJECTS with Error('Scanner cancelled'); scanner failures
  // reject with the host's error message.
}
```

Mobile-only. On desktop (or when the merchant dismisses the scanner) the
`capture()` promise **rejects** with `Error('Scanner cancelled')` — there
is no `cancelled` field on a resolved payload.

### Print (`PRINT`)

```js theme={null}
import { Print } from '@launchmystore/app-bridge';

Print.create(app).dispatch();
// or
app.dispatch('PRINT');
```

### Share (`SHARE`)

```js theme={null}
import { Share } from '@launchmystore/app-bridge';

Share.create(app, {
  title: 'Check out this product',
  url: 'https://store.com/products/widget',
  text: 'Best socks ever',
}).dispatch();
```

## Lifecycle

```js theme={null}
import { Lifecycle } from '@launchmystore/app-bridge';

const lifecycle = Lifecycle.create(app);
const off = lifecycle.subscribe('visible', () => loadData());
lifecycle.subscribe('hidden', () => saveState());

// Or several at once:
lifecycle.subscribeAll({
  focus:  () => console.log('focused'),
  blur:   () => console.log('blurred'),
});

lifecycle.isFocused();   // boolean helpers
lifecycle.isVisible();
```

Events: `'focus' | 'blur' | 'visible' | 'hidden'` — driven by the
iframe's own `focus`/`blur`/`visibilitychange` plus the host-pushed
`APP_FOCUS` / `APP_BLUR` / `APP_VISIBLE` / `APP_HIDDEN` actions. There
are no mount/unmount events — run mount logic in your own bootstrap.

## Checkout-only Surfaces

The following actions are wired into the customer-checkout host and the
post-purchase host.
See [Checkout App Bridge](/app-bridge/checkout) for the full contract.

### Cart (`CART_LINES_CHANGE`, `DISCOUNT_CODE_CHANGE`, `NOTE_CHANGE`, `ATTRIBUTE_CHANGE`, `GIFT_CARD_CHANGE`)

```js theme={null}
import { Cart } from '@launchmystore/app-bridge';

const cart = Cart.create(app);

await cart.applyCartLinesChange({
  type: 'addCartLine',
  merchandiseId: 'gid://launchmystore/Variant/abc-123',
  quantity: 1,
});

await cart.applyDiscountCodeChange({
  type: 'addDiscountCode',
  code: 'WELCOME10',
});

await cart.applyNoteChange({
  type: 'updateNote',
  note: 'Leave with concierge',
});

await cart.applyAttributeChange({
  type: 'updateAttribute',
  key: 'gift_wrap',
  value: 'true',
});
```

### Buyer Journey (`BUYER_JOURNEY_INTERCEPT_REQUEST/RESPONSE`)

Block the customer from advancing to payment until your condition is met.

```js theme={null}
import { BuyerJourney } from '@launchmystore/app-bridge';

const journey = BuyerJourney.create(app);
const unsubscribe = journey.intercept(() => {
  if (!termsAccepted) {
    return {
      behavior: 'block',
      reason: 'You must accept the terms before continuing.',
    };
  }
  return { behavior: 'allow' };
});

// later
unsubscribe();
```

## Error Handling

```js theme={null}
try {
  const result = await app.dispatchAndWait('RESOURCE_PICKER_OPEN', {
    resourceType: 'product',
  });
} catch (error) {
  // Timeouts reject with Error('App Bridge: timeout waiting for <ACTION> response');
  // host-reported failures reject with the host's error string verbatim.
  if (error.message.includes('timeout')) {
    // Host did not respond — likely action not supported on this host
  } else {
    console.error(error);
  }
}
```

The SDK rejects with a timeout error after **10 seconds** if the host never
responds. That's the most common failure mode when calling an admin-only
action from a checkout host (or vice versa) — check the table at the top of
this page for which host each action is wired to.

## Complete Example

```js theme={null}
import {
  createApp, Toast, ResourcePicker, ContextualSaveBar,
} from '@launchmystore/app-bridge';

const app = createApp({
  apiKey: 'your-client-id',
  host: new URLSearchParams(location.search).get('host'),
});

document.getElementById('add-review').addEventListener('click', async () => {
  // Raw round-trip: resolves with { pickerId, selection }
  const picker = await app.dispatchAndWait('RESOURCE_PICKER_OPEN', {
    resourceType: 'product',
  });
  if (picker.selection.length === 0) return;   // empty = cancelled

  app.dispatch('LOADING_START');
  try {
    const token = await app.getSessionToken();
    await fetch('/api/reviews', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ productId: picker.selection[0].id }),
    });
    Toast.success(app, 'Review added!');
  } catch (err) {
    Toast.error(app, 'Failed to add review');
  } finally {
    app.dispatch('LOADING_STOP');
  }
});
```

## I18n (`I18N_UPDATE`)

The host pushes the merchant's admin locale to your iframe and re-pushes
on every change. Subscribe via the `I18n` action class — it caches the
last dictionary and exposes `translate` / `formatNumber` / `formatDate`
helpers backed by `Intl.*`.

```js theme={null}
import { I18n } from '@launchmystore/app-bridge';

const i18n = I18n.create(app);
i18n.subscribe(({ locale, dictionary }) => {
  console.log('Locale changed to', locale);
});

// Plain interpolation map (no reserved keys → second arg is treated as vars).
const greeting = i18n.translate('hello', { name: 'Alex' });

// Options form: supply a fallback string when the key is missing AND
// interpolation vars under `vars`. The fallback wins over the bare key.
const label = i18n.translate('settings.title', {
  default: 'Settings',
  vars: { user: 'Alex' },
});

const price = i18n.formatNumber(19.99, { style: 'currency', currency: 'USD' });
```

The wire payload is `{ locale: string, dictionary: Record<string,string> }`.
The host derives `locale` from the admin's cookie/localStorage and the
`dictionary` from `/api/i18n/admin?locale=...` (silently `{}` if the
endpoint is absent — your app should fall back to `Intl.*` for
formatting).

## REST API (`CONFIG_GET` + session tokens)

Use the `RestApi` action class to call the LaunchMyStore REST API
(`/api/v1/...`) with an automatically-attached session token. The host
exposes the API base URL via `CONFIG_GET` so your iframe doesn't have to
hardcode `api.launchmystore.io`.

```js theme={null}
import { RestApi } from '@launchmystore/app-bridge';

const api = RestApi.create(app);
const { products } = await api.fetchJson('/api/v1/products?limit=50');

// Plain-object bodies are auto-JSON-encoded; `Content-Type` is set unless
// you provide one yourself. Pass a string / FormData / Blob to opt out.
await api.fetch('/api/v1/products/123', {
  method: 'PUT',
  body: { title: 'New title' },
});
```

`fetch()` resolves to the raw `Response`. `fetchJson<T>()` parses JSON
and returns the typed body. Token + apiBase are resolved lazily on the
first call and cached.

## Intents (`INTENT_LAUNCH`)

Launch another app's admin action from inside your own — for example,
chain a "Send invoice" action after editing an order.

```js theme={null}
import { Intents } from '@launchmystore/app-bridge';

const intents = Intents.create(app);
const result = await intents.launchAndWait('invoice-app.send-invoice', {
  orderId: 'gid://launchmystore/Order/123',
});

if (result.launched) {
  console.log('Opened intent', result.target);
}
```

The target string is `<appHandle>.<extensionHandle>` — the host resolves
it against the merchant's registered admin extensions and opens the
matching `admin_action` modal. Missing targets return `{ error: '...' }`.

## Action constants

The SDK does not export a centralised `ACTIONS` enum — every action name is
a plain string passed to `app.dispatch` / `app.dispatchAndWait`. The
canonical source of truth for each name is the corresponding action class
itself:

```ts theme={null}
import {
  Toast,             // 'TOAST_SHOW', 'TOAST_DISMISS'
  Modal,             // 'MODAL_OPEN', 'MODAL_CLOSE'
  ResourcePicker,    // 'RESOURCE_PICKER_OPEN', 'RESOURCE_PICKER_CLOSE'
  TitleBar,          // 'TITLE_BAR_UPDATE'
  NavigationMenu,    // 'NAV_MENU_UPDATE'
  ContextualSaveBar, // 'CONTEXTUAL_SAVE_BAR'
  Redirect,          // 'REDIRECT'
  Fullscreen,        // 'FULLSCREEN_ENTER' | 'EXIT' | 'TOGGLE'
  LeaveConfirmation, // 'LEAVE_CONFIRMATION_ENABLE' | 'DISABLE'
  Scanner,           // 'SCANNER_OPEN', 'SCANNER_CLOSE'
  User,              // 'USER_FETCH'
  Config,            // 'CONFIG_FETCH' (admin) / 'CONFIG_GET' (RestApi)
  Environment,       // 'ENVIRONMENT_FETCH'
  Features,          // 'FEATURES_QUERY'
  Clipboard,         // 'CLIPBOARD_READ_REQUEST' (writes run iframe-side)
  Print,             // 'PRINT'
  Share,             // 'SHARE'
  History,           // 'HISTORY_PUSH' | 'REPLACE' | 'BACK' | 'FORWARD' | 'GO'
  Lifecycle,         // host-pushed 'APP_FOCUS' | 'APP_BLUR' | 'APP_VISIBLE' | 'APP_HIDDEN'
  SessionToken,      // 'SESSION_TOKEN_REQUEST'
  Cart,              // 'CART_LINES_CHANGE' | 'DISCOUNT_CODE_CHANGE'
                     // | 'GIFT_CARD_CHANGE'  | 'NOTE_CHANGE'
                     // | 'ATTRIBUTE_CHANGE'  | 'METAFIELD_CHANGE'
  BuyerJourney,      // 'BUYER_JOURNEY_INTERCEPT_REQUEST/RESPONSE'
  I18n,              // 'I18N_UPDATE'           (host-pushed)
  RestApi,           // 'CONFIG_GET' (resolves apiBase)
  Intents,           // 'INTENT_LAUNCH'
} from '@launchmystore/app-bridge';
```

`APP_BRIDGE_FEATURES` (exported from the package root) is the one named
constant the SDK ships — a readonly **array** of PascalCase feature names
(`'Toast'`, `'Modal'`, `'ResourcePicker'`, …). Use it with the local
`Features` helper:

```ts theme={null}
import { Features, APP_BRIDGE_FEATURES } from '@launchmystore/app-bridge';

const features = Features.create(app);
if (features.isAvailable('ResourcePicker')) {
  // Safe to call ResourcePicker
}
APP_BRIDGE_FEATURES.includes('Scanner');   // array membership check
```

If you need a raw string action name that no class wraps yet (rare —
this only happens for in-flight features) call `app.dispatch(name, ...)`
directly. Action names are case-sensitive and the host silently ignores
unknown names, so a typo will surface as a 10-second timeout in
`dispatchAndWait`. See [Error Handling](/app-bridge/error-handling) for
the recovery pattern.

## See Also

* [App Bridge Overview](/app-bridge/overview) — wire format, security, and SDK initialization
* [App Bridge for Checkout](/app-bridge/checkout) — Cart, BuyerJourney, and other checkout-only actions in depth
* [Post-Purchase Bridge](/app-bridge/post-purchase-bridge) — action-by-action reference for `host: 'post-purchase'` iframes
* [Error Handling](/app-bridge/error-handling) — timeouts, rejections, and per-action failure modes
* [React Hooks](/app-bridge/react-hooks) — `useAppBridge()`, `useToast()`, `useResourcePicker()`, etc.
* [Session Tokens](/app-bridge/session-tokens) — how `app.getSessionToken()` works
