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

# React Hooks

> React hooks for App Bridge integration

# React Hooks for App Bridge

`@launchmystore/app-bridge-react` ships a typed hook for every App Bridge
action family. Each hook wraps the underlying SDK action class from
`@launchmystore/app-bridge/actions`, so the wire protocol stays exactly the
same — you just don't have to construct messages by hand.

## Installation

```bash theme={null}
npm install @launchmystore/app-bridge @launchmystore/app-bridge-react
```

## AppBridgeProvider

Mount once near the root of your iframe app:

```jsx theme={null}
import { AppBridgeProvider } from '@launchmystore/app-bridge-react';

function Root() {
  return (
    <AppBridgeProvider
      config={{
        apiKey: process.env.NEXT_PUBLIC_APP_CLIENT_ID,
        host: new URLSearchParams(location.search).get('host'),
      }}
    >
      <MyApp />
    </AppBridgeProvider>
  );
}
```

<Warning>
  `AppBridgeProvider` takes a **single `config` prop** (`{ apiKey, host }`).
  The earlier docs showed `apiKey` / `host` as separate props — that form is
  not supported.
</Warning>

The provider creates one `App` instance, tears it down on unmount, and
hands it to every hook through a React context. Calling any hook outside
the provider throws `useAppBridge must be used within an AppBridgeProvider`.

## Hook Index

| Family          | Hooks                                                                                                                                                                                                   |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Core            | `useAppBridge`                                                                                                                                                                                          |
| Auth            | `useSessionToken`, `useAuthenticatedFetch`, `useAppQuery`, `useAppMutation`                                                                                                                             |
| UI surface      | `useToast`, `useModal`, `useConfirmationModal`, `useTitleBar`, `useNavigationMenu`, `useContextualSaveBar`, `useDirtyState`, `useLoading`, `useFullscreen`, `useLeaveConfirmation`, `useUnsavedChanges` |
| Resource picker | `useResourcePicker`, `useProductPicker`, `useCollectionPicker`, `useCustomerPicker`, `useFilePicker`                                                                                                    |
| Navigation      | `useRedirect`                                                                                                                                                                                           |
| Browser APIs    | `usePrint`, `useShare`, `useClipboard`, `useCopyToClipboard`, `useHistory`                                                                                                                              |
| Lifecycle       | `useLifecycle`, `useOnFocus`, `useOnBlur`, `useOnVisible`, `useOnHidden`                                                                                                                                |
| Data APIs       | `useUser`, `useConfig`, `useEnvironment`, `useFeatures`                                                                                                                                                 |
| Subscriptions   | `useAppSubscription`, `useAppDispatch`, `useAppDispatchAndWait`                                                                                                                                         |
| Checkout        | `useCart`                                                                                                                                                                                               |

## Core

### useAppBridge

Access the raw `App` instance — useful when a built-in hook doesn't cover
your case or you want to call `app.dispatch(action, payload)` directly.

```jsx theme={null}
import { useAppBridge } from '@launchmystore/app-bridge-react';

function CustomButton() {
  const app = useAppBridge();
  return (
    <button onClick={() => app.dispatch('TOAST_SHOW', { message: 'Hi!' })}>
      Show toast
    </button>
  );
}
```

## Authentication

### useSessionToken

Manages a cached JWT for authenticated backend calls. The hook fetches one
token on mount and exposes `getToken()` (returns the cached token unless
it's near expiry, then refreshes) plus `refresh()` (forces a new token).

```jsx theme={null}
import { useSessionToken } from '@launchmystore/app-bridge-react';

function Reviews() {
  const { token, loading, error, getToken, refresh } = useSessionToken();

  const onClick = async () => {
    const currentToken = await getToken();
    await fetch('/api/reviews', {
      headers: { Authorization: `Bearer ${currentToken}` },
    });
  };

  if (loading) return <div>Authenticating…</div>;
  if (error)   return <div>Auth error: {error.message}</div>;

  return <button onClick={onClick}>Load reviews</button>;
}
```

Return shape:

| Field        | Type                    | Description                                                                      |
| ------------ | ----------------------- | -------------------------------------------------------------------------------- |
| `token`      | `string \| null`        | The current cached token.                                                        |
| `loading`    | `boolean`               | True until the first token resolves.                                             |
| `error`      | `Error \| null`         | Error from the initial fetch.                                                    |
| `getToken()` | `() => Promise<string>` | Resolve with a valid token (cached if fresh, refreshed if within 30s of expiry). |
| `refresh()`  | `() => Promise<string>` | Force a fresh round-trip to the host.                                            |

### useAuthenticatedFetch, useAppQuery, useAppMutation

Helpers built on top of `useSessionToken`. `useAuthenticatedFetch()`
returns a wrapped `fetch` that injects the Bearer header automatically:

```jsx theme={null}
const authFetch = useAuthenticatedFetch();
const res = await authFetch('/api/orders');
```

See [Sessions & Authentication](/app-bridge/session-tokens) for the data-
fetching variants (`useAppQuery`, `useAppMutation`).

## UI Surface

### useToast

Returns an object with `show`, `success`, `error`, `warning`, `info` — not
a callable. Toast options accept `{ message, duration?, type? }`.

```jsx theme={null}
import { useToast } from '@launchmystore/app-bridge-react';

function SaveButton() {
  const toast = useToast();

  const onSave = async () => {
    try {
      await saveData();
      toast.success('Saved!');
    } catch {
      toast.error('Save failed');
    }
  };

  return <button onClick={onSave}>Save</button>;
}
```

```typescript theme={null}
interface UseToastReturn {
  show:    (opts: { message: string; duration?: number; type?: 'success' | 'error' | 'warning' | 'info' }) => void;
  success: (message: string, duration?: number) => void;
  error:   (message: string, duration?: number) => void;
  warning: (message: string, duration?: number) => void;
  info:    (message: string, duration?: number) => void;
}
```

<Note>
  The admin host reads `payload.type` for the toast variant. The React
  checkout host reads `payload.variant`. The `useToast` hook always sends
  `type` — if you're posting toasts from a checkout iframe, build the
  payload manually with `app.dispatch('TOAST_SHOW', { message, variant })`.
</Note>

### useModal

Opens a non-promise modal whose button callbacks fire as you set them.

```jsx theme={null}
import { useModal } from '@launchmystore/app-bridge-react';

function DeleteButton({ onConfirm }) {
  const modal = useModal({
    title: 'Delete item?',
    message: 'This cannot be undone.',
    primaryAction:   { label: 'Delete', onAction: onConfirm },
    secondaryActions: [{ label: 'Cancel' }],
    onClose: () => console.log('modal closed'),
  });

  return <button onClick={modal.open}>Delete</button>;
}
```

Returns `{ open(): void; close(): void }`. Each `open()` call creates and
dispatches a fresh modal instance; `close()` dismisses the last one.

### useConfirmationModal

Promise-returning variant — perfect for inline `await`.

```jsx theme={null}
import { useConfirmationModal } from '@launchmystore/app-bridge-react';

function DeleteButton({ id }) {
  const confirm = useConfirmationModal();

  const onClick = async () => {
    const ok = await confirm({
      title:        'Delete review',
      message:      'Permanently remove this review?',
      confirmLabel: 'Delete',
      cancelLabel:  'Keep',
      size:         'small',  // 'small' | 'medium' | 'large'
    });
    if (ok) await deleteReview(id);
  };

  return <button onClick={onClick}>Delete</button>;
}
```

### useTitleBar

Configures the host's title bar. Returns `update()` (merges new options
into the live title bar) and `setPrimaryLoading()` for the common case of
showing a spinner on the primary button during async work.

```jsx theme={null}
const titleBar = useTitleBar({
  title: 'Edit product',
  primaryAction: {
    label: 'Save',
    onAction: async () => {
      titleBar.setPrimaryLoading(true);
      await saveProduct();
      titleBar.setPrimaryLoading(false);
    },
  },
  secondaryActions: [
    { label: 'Duplicate', onAction: dup },
    { label: 'Delete',   onAction: del, destructive: true },
  ],
  breadcrumbs: [{ label: 'Products', url: '/admin/products' }],
});
```

### useNavigationMenu

Declares the menu rendered by the admin chrome when your app is mounted.
Provide an items array; the host renders them next to the page title.

```jsx theme={null}
import { useNavigationMenu } from '@launchmystore/app-bridge-react';

function AppNav() {
  useNavigationMenu({
    items: [
      { label: 'Dashboard', destination: '/' },
      { label: 'Reviews',   destination: '/reviews' },
      { label: 'Settings',  destination: '/settings' },
    ],
    active: '/reviews',
  });
  return null;
}
```

### useContextualSaveBar / useDirtyState

`useContextualSaveBar` is the low-level hook — call `show()` / `hide()` /
`update()` / `setSaveLoading()` / `setDiscardLoading()` directly.

```jsx theme={null}
const saveBar = useContextualSaveBar({
  message:  'Unsaved changes',
  onSave:   async () => { await saveSettings(); },
  onDiscard: () => resetSettings(),
});

useEffect(() => {
  if (isDirty) saveBar.show();
  else         saveBar.hide();
}, [isDirty]);
```

`useDirtyState` glues that together with an internal dirty flag:

```jsx theme={null}
const { isDirty, setDirty } = useDirtyState({
  onSave:   () => saveSettings(),
  onDiscard: () => resetSettings(),
});

<input onChange={(e) => { update(e.target.value); setDirty(true); }} />
```

### useLoading

Drives the host's global thin-progress-bar via the `LOADING_START` /
`LOADING_STOP` actions. Use `wrap()` for the common pattern.

```jsx theme={null}
import { useLoading } from '@launchmystore/app-bridge-react';

function FetchButton() {
  const loading = useLoading();

  const onClick = () =>
    loading.wrap(async () => {
      const data = await fetch('/api/heavy').then((r) => r.json());
      // ...
    });

  return <button onClick={onClick}>Fetch</button>;
}
```

Returns `{ start, stop, wrap<T>(fn: () => Promise<T>): Promise<T> }`.

### useFullscreen

```jsx theme={null}
const { isFullscreen, enter, exit, toggle } = useFullscreen();
```

`isFullscreen` flips to `true` when the host posts back
`FULLSCREEN_ENTERED` (similarly `false` on `FULLSCREEN_EXITED`), so you
can mirror the host's actual state instead of guessing.

### useLeaveConfirmation / useUnsavedChanges

`useLeaveConfirmation` exposes manual `enable()` / `disable()` /
`setMessage()`. `useUnsavedChanges` wraps it with a `setDirty(boolean)`
helper that enables/disables in lock-step with form state.

```jsx theme={null}
const { isDirty, setDirty } = useUnsavedChanges('You have unsaved changes.');

<form onChange={() => setDirty(true)}>…</form>
```

Both hooks also wire the browser's native `beforeunload` event under the
hood, so closing the tab triggers the same confirmation.

## Resource Picker

### useResourcePicker

Generic resource picker — pass `resourceType` plus optional `multiple`,
`initialSelectionIds`, `filter`, `onSelect`, `onCancel`.

```jsx theme={null}
import { useResourcePicker } from '@launchmystore/app-bridge-react';

function ProductSelector({ onPick }) {
  const picker = useResourcePicker({
    resourceType: 'product',         // or 'Product'
    multiple: true,
    onSelect: (selection) => onPick(selection),  // [{ id, … }, …]
    onCancel: () => console.log('cancelled'),
  });

  return <button onClick={picker.open}>Pick products</button>;
}
```

Returns `{ open(): void; close(): void }`.

### Typed convenience hooks

```jsx theme={null}
import {
  useProductPicker,
  useCollectionPicker,
  useCustomerPicker,
  useFilePicker,
} from '@launchmystore/app-bridge-react';
```

Each takes the same options as `useResourcePicker` minus `resourceType`.

<Note>
  There is no `useOrderPicker` / `useArticlePicker` / `useMenuPicker`. For
  those, call the generic `useResourcePicker({ resourceType: 'order' })`.
  The full type list lives in [Actions
  Reference](/app-bridge/actions#resource-types).
</Note>

## Navigation

### useRedirect

Returns a menu of typed navigation helpers — the generic `navigate()`
plus `open()` (new tab) and one helper per Admin resource detail page.

```jsx theme={null}
import { useRedirect } from '@launchmystore/app-bridge-react';

function Links({ productId }) {
  const r = useRedirect();
  return (
    <>
      <button onClick={() => r.toProduct(productId)}>View product</button>
      <button onClick={() => r.navigate('/admin/orders')}>Orders</button>
      <button onClick={() => r.open('https://docs.launchmystore.io')}>Docs (new tab)</button>
    </>
  );
}
```

```typescript theme={null}
interface UseRedirectReturn {
  navigate:     (url: string) => void;
  open:         (url: string) => void;             // newContext: true
  toApp:        (path?: string) => void;
  toAdmin:      (path?: string) => void;
  toProduct:    (productId: string) => void;
  toCollection: (collectionId: string) => void;
  toOrder:      (orderId: string) => void;
  toCustomer:   (customerId: string) => void;
}
```

## Browser APIs

### useClipboard

`copy()` writes through the browser's `navigator.clipboard.writeText` API.
`paste()` round-trips to the host (`CLIPBOARD_READ_REQUEST`) — required
because Chrome blocks `clipboard.readText()` in cross-origin iframes.

```jsx theme={null}
const { copy, paste, copied, loading, error, isAvailable, reset } = useClipboard();

<button onClick={() => copy('SKU-12345').then(() => setTimeout(reset, 2000))}>
  {copied ? 'Copied!' : 'Copy SKU'}
</button>
```

`useCopyToClipboard()` is the slim variant — just `(text) => Promise<void>`.

### usePrint

`usePrint()` returns a single function: `print() => void`. The host opens
the browser print dialog scoped to your iframe.

### useShare

Wraps the Web Share API (mobile). `useShare()` returns
`{ share: (data: { title, url, text? }) => Promise<void>; isSupported }`.

### useHistory

Drives the host browser history — `push`, `replace`, `go`, `back`,
`forward`. Useful when your app uses its own router and you want
host-aware history.

## Lifecycle

`useLifecycle({ onFocus, onBlur, onVisible, onHidden })` plus the four
single-event variants `useOnFocus`, `useOnBlur`, `useOnVisible`,
`useOnHidden`. Fired when the host detects the iframe has focused,
blurred, become visible, or been hidden.

```jsx theme={null}
useOnVisible(() => refetchData());  // refresh when the merchant comes back
```

## Data APIs

| Hook               | Returns                                                   | When to use                                         |
| ------------------ | --------------------------------------------------------- | --------------------------------------------------- |
| `useUser()`        | `{ user, loading, error, refresh }`                       | Get the merchant user record (id, email, roles).    |
| `useConfig()`      | `{ config, loading, error, refresh }`                     | App-specific config the merchant set.               |
| `useEnvironment()` | `{ environment, loading, error, refresh }`                | Host environment info: locale, currency, host kind. |
| `useFeatures()`    | `{ features, loading, error, hasFeature(name), refresh }` | Feature flags exposed by the host.                  |

All four are RPCs the first time you call them and cache the result;
`refresh()` forces a round-trip.

## Subscriptions

Three escape hatches when no typed hook exists for what you need:

* `useAppSubscription(action, callback)` — subscribe to a host-initiated
  action (e.g. `MODAL_PRIMARY_ACTION`). Returns nothing; cleanup happens
  on unmount.
* `useAppDispatch()` — returns the bound `(action, payload?) => string`
  function from the underlying App.
* `useAppDispatchAndWait()` — returns the bound
  `(action, payload?) => Promise<payload>` function.

```jsx theme={null}
import { useAppSubscription, useAppDispatchAndWait } from '@launchmystore/app-bridge-react';

function CartPanel() {
  const dispatchAndWait = useAppDispatchAndWait();

  useAppSubscription('CART_UPDATED', () => {
    // Host emitted a cart-updated event — refetch
    dispatchAndWait('CART_GET').then(setCart);
  });

  // …
}
```

## Checkout

### useCart

Returns a memoised `Cart` action instance for the checkout host. Use it
inside checkout extensions to mutate the host cart:

```jsx theme={null}
import { useCart } from '@launchmystore/app-bridge-react';

function AddUpsell() {
  const cart = useCart();

  const add = () =>
    cart.applyCartLinesChange({
      type: 'addCartLine',
      merchandiseId: 'gid://launchmystore/Variant/abc',
      quantity: 1,
    });

  return <button onClick={add}>Add gift wrap</button>;
}
```

See [App Bridge for Checkout](/app-bridge/checkout) for the full payload
shapes (`applyDiscountCodeChange`, `applyNoteChange`,
`applyAttributeChange`, `applyGiftCardChange`, etc.).

## Complete Example

A realistic admin block that combines auth, save bar, resource picker,
title bar, and the loading indicator:

```jsx theme={null}
import {
  AppBridgeProvider,
  useSessionToken,
  useToast,
  useConfirmationModal,
  useLoading,
  useTitleBar,
  useProductPicker,
  useDirtyState,
} from '@launchmystore/app-bridge-react';
import { useEffect, useState } from 'react';

function Root() {
  return (
    <AppBridgeProvider
      config={{
        apiKey: process.env.NEXT_PUBLIC_APP_CLIENT_ID,
        host:   new URLSearchParams(location.search).get('host'),
      }}
    >
      <ReviewsPanel />
    </AppBridgeProvider>
  );
}

function ReviewsPanel() {
  const [selected, setSelected] = useState(null);
  const { getToken } = useSessionToken();
  const toast       = useToast();
  const confirm     = useConfirmationModal();
  const loading     = useLoading();

  const picker = useProductPicker({
    multiple: false,
    onSelect: ([product]) => { setSelected(product); setDirty(true); },
  });

  const { setDirty } = useDirtyState({
    onSave: () =>
      loading.wrap(async () => {
        const token = await getToken();
        await fetch('/api/reviews', {
          method:  'POST',
          headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
          body:    JSON.stringify({ productId: selected.id }),
        });
        toast.success('Saved');
      }),
    onDiscard: () => { setSelected(null); },
  });

  useTitleBar({
    title:         'Product reviews',
    primaryAction: { label: 'Pick product', onAction: picker.open },
  });

  const onDelete = async () => {
    if (!(await confirm({ title: 'Delete review?', message: 'Cannot be undone.' }))) return;
    toast.success('Deleted');
  };

  return (
    <div>
      {selected ? <p>Reviewing: {selected.title}</p> : <p>No product picked</p>}
      <button onClick={onDelete}>Delete</button>
    </div>
  );
}
```

## TypeScript

Every hook ships with typed return values, options, and accompanying
types — `UseToastReturn`, `UseModalOptions`, `UseResourcePickerReturn`,
`UseSessionTokenReturn`, etc. Import from the package root.

```ts theme={null}
import type {
  UseToastReturn,
  UseModalOptions,
  UseResourcePickerOptions,
  UseSessionTokenReturn,
} from '@launchmystore/app-bridge-react';
```

## `useI18n`

Subscribes to host-driven locale changes and exposes `translate`,
`formatNumber`, `formatDate` helpers (backed by `Intl.*`).

```tsx theme={null}
import { useI18n } from '@launchmystore/app-bridge-react';

function SettingsHeader() {
  const { locale, translate, formatNumber } = useI18n();
  return (
    <>
      <h1>{translate('settings.title', { default: 'Settings', vars: {} })}</h1>
      <p>Subtotal: {formatNumber(19.99, { style: 'currency', currency: 'USD' })}</p>
      <small>Locale: {locale}</small>
    </>
  );
}
```

The hook re-renders whenever the host pushes a new `I18N_UPDATE`, so
locale changes from the admin chrome propagate without manual handling.

## `useRestApi`

Returns authenticated `fetch` / `fetchJson` helpers for the LaunchMyStore
REST API. Session token and API base URL are resolved lazily and
cached.

```tsx theme={null}
import { useRestApi } from '@launchmystore/app-bridge-react';

function ProductCount() {
  const { fetchJson } = useRestApi();
  const [count, setCount] = useState<number | null>(null);

  useEffect(() => {
    fetchJson<{ products: unknown[] }>('/api/v1/products?limit=250')
      .then((d) => setCount(d.products.length));
  }, [fetchJson]);

  return <p>You have {count ?? '…'} products.</p>;
}
```

Use REST verbs (GET / POST / PUT / PATCH / DELETE) against `/api/v1/...`
paths. The hook handles `Authorization: Bearer <token>` and resolves the
base URL from `CONFIG_GET` so your iframe works in dev, staging, and
production.

## `useIntents`

Launches another app's admin action by intent target.

```tsx theme={null}
import { useIntents } from '@launchmystore/app-bridge-react';

function SendInvoiceButton({ orderId }: { orderId: string }) {
  const intents = useIntents();
  return (
    <button onClick={async () => {
      const result = await intents.launchAndWait('invoice-app.send-invoice', { orderId });
      if (result.error) alert(`Couldn't launch: ${result.error}`);
    }}>
      Send invoice
    </button>
  );
}
```

## `useApi` — the umbrella hook

For pages that need most of the App Bridge surface, `useApi()` returns a
single object containing every helper:

```tsx theme={null}
import { useApi } from '@launchmystore/app-bridge-react';

function ProductDetailExtension() {
  const { i18n, restApi, picker, toast, modal, navigation, intents, data } = useApi();
  // …
}
```

This is the recommended entry point for admin block / admin action
React components — one shared API surface, automatic listener cleanup,
and stable references across renders.

## See Also

* [React Components](/app-bridge/components) — `<TitleBar />`,
  `<NavigationMenu />`, `<AdminBlock>`, `<AdminAction>`, layout/text/form
  primitives and design tokens.
* [Actions Reference](/app-bridge/actions) — payload shapes for every action.
* [Sessions & Authentication](/app-bridge/session-tokens) — token verification on your backend.
* [App Bridge for Checkout](/app-bridge/checkout) — `useCart` payload reference.
* [App Bridge Overview](/app-bridge/overview) — raw SDK if you don't want React.
