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:
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? }.
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
const result = await ResourcePicker.create(app, { resourceType: 'product' }).dispatch();
// Request/response — raw
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 |
| 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_WRITE, CLIPBOARD_READ_REQUEST |
| 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 | LIFECYCLE_MOUNT, LIFECYCLE_UNMOUNT |
| 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 |
Cart and BuyerJourney are checkout-only. They’re documented in detail on the
Checkout App Bridge page. Everything else runs in the
admin host.
UI Actions
Toast (TOAST_SHOW)
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)
import { Modal } from '@launchmystore/app-bridge';
const result = await Modal.create(app, {
title: 'Delete Product?',
message: 'This cannot be undone.',
primaryAction: { content: 'Delete', destructive: true },
secondaryAction: { content: 'Cancel' },
}).dispatch();
if (result.action === 'primary') {
// user clicked Delete
}
// Raw
const result2 = await app.dispatchAndWait('MODAL_OPEN', {
title: 'Confirm',
message: 'Are you sure?',
primaryAction: { content: 'OK' },
});
Response: { action: 'primary' | 'secondary' | 'dismissed' }
Loading (LOADING_START / LOADING_STOP)
app.dispatch('LOADING_START');
await save();
app.dispatch('LOADING_STOP');
Fullscreen (FULLSCREEN_ENTER / EXIT / TOGGLE)
import { Fullscreen, FullscreenAction } from '@launchmystore/app-bridge';
Fullscreen.create(app, FullscreenAction.Enter).dispatch();
// or
app.dispatch('FULLSCREEN_ENTER');
Title Bar (TITLE_BAR_UPDATE)
import { TitleBar } from '@launchmystore/app-bridge';
TitleBar.create(app, {
title: 'Product Reviews',
primaryAction: { content: 'Add Review', onAction: 'ADD_REVIEW' },
secondaryActions: [
{ content: 'Export', onAction: 'EXPORT' },
{ content: 'Settings', onAction: 'SETTINGS' },
],
breadcrumbs: [
{ content: 'Apps', url: '/admin/apps' },
{ content: 'My App', url: '/admin/apps/my-app' },
],
}).dispatch();
// Listen for button clicks
app.subscribe('ADD_REVIEW', () => console.log('Add clicked'));
Navigation
Redirect (REDIRECT)
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,
});
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)
import { History } from '@launchmystore/app-bridge';
History.create(app).push({ path: '/reviews?filter=pending' });
History.create(app).replace({ path: '/reviews' });
History.create(app).back();
History.create(app).forward();
History.create(app).go(-2);
Resource Picker (RESOURCE_PICKER_OPEN / CLOSE)
import { ResourcePicker } from '@launchmystore/app-bridge';
const result = await ResourcePicker.create(app, {
resourceType: 'product',
multiple: true,
selectionIds: ['prod_123', 'prod_987'], // preferred
filter: { status: 'active' },
}).dispatch();
if (!result.cancelled) {
console.log('Selected:', result.selection);
}
multiple: true lets the picker return more than one resource. Pass
selectionIds (an array of ids) to pre-select. Legacy
initialSelection: [{ id }] still works but selectionIds is the
recommended shape going forward. On cancel the picker returns
{ cancelled: true, selection: [] } — selection is always an array.
| 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: { cancelled: boolean, selection: [...] } — selection is always an
array (length 1 for single-select).
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:
{ "id": "<mediaId>", "url": "https://cdn…/asset.png", "filename": "asset.png", "mimeType": "image/png" }
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.
Save Bar (CONTEXTUAL_SAVE_BAR)
import { ContextualSaveBar, SaveBarAction } from '@launchmystore/app-bridge';
const bar = ContextualSaveBar.create(app, {
saveAction: { content: 'Save', onAction: 'SAVE' },
discardAction: { content: 'Discard', onAction: 'DISCARD' },
fullWidth: false,
});
bar.show();
app.subscribe('SAVE', async () => {
await saveChanges();
bar.hide();
});
app.subscribe('DISCARD', () => {
discardChanges();
bar.hide();
});
The raw payload is { action: 'SHOW' | 'HIDE', ... } — use the class.
Leave Confirmation (LEAVE_CONFIRMATION_ENABLE / DISABLE)
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)
// Preferred — handles caching + auto-refresh
const token = await app.getSessionToken();
fetch('/api/v1/data', {
headers: { Authorization: `Bearer ${token}` },
});
Raw dispatchAndWait('SESSION_TOKEN_REQUEST') returns { token, expires }
but the wrapper caches the token to avoid round-trips on every API call.
User / Config / Environment / Features
import { User, Config, Environment, Features } from '@launchmystore/app-bridge';
const user = await User.create(app).fetch();
// { id, email, name, roles }
const config = await Config.create(app).fetch();
// { apiKey, shopDomain, plan }
const env = await Environment.create(app).fetch();
// { locale, timezone, mobile }
const flags = await Features.create(app).query();
// { contextual_save_bar: true, resource_picker: true, ... }
Raw action names: USER_FETCH, CONFIG_FETCH, ENVIRONMENT_FETCH,
FEATURES_QUERY.
Browser APIs
Clipboard (CLIPBOARD_WRITE / CLIPBOARD_READ_REQUEST)
import { Clipboard } from '@launchmystore/app-bridge';
await Clipboard.write(app, 'Copied!');
const text = await Clipboard.read(app);
Reads go through the host because Chrome blocks clipboard-read in
cross-origin iframes. Writes (copy()) work iframe-side directly.
Scanner (SCANNER_OPEN / SCANNER_CLOSE)
import { Scanner } from '@launchmystore/app-bridge';
const result = await Scanner.create(app).open();
if (!result.cancelled) console.log('Scanned:', result.data);
Mobile-only. Desktop hosts respond with cancelled: true.
Print (PRINT)
import { Print } from '@launchmystore/app-bridge';
Print.create(app).dispatch();
// or
app.dispatch('PRINT');
Share (SHARE)
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
import { Lifecycle } from '@launchmystore/app-bridge';
Lifecycle.create(app).onMount(() => loadData());
Lifecycle.create(app).onUnmount(() => saveState());
Raw event names: LIFECYCLE_MOUNT, LIFECYCLE_UNMOUNT.
Checkout-only Surfaces
The following actions are wired into the customer-checkout host and the
post-purchase host.
See Checkout App Bridge for the full contract.
Cart (CART_LINES_CHANGE, DISCOUNT_CODE_CHANGE, NOTE_CHANGE, ATTRIBUTE_CHANGE, GIFT_CARD_CHANGE)
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.
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
try {
const result = await app.dispatchAndWait('RESOURCE_PICKER_OPEN', {
resourceType: 'product',
});
} catch (error) {
// App Bridge errors arrive as Error('App Bridge: ACTION failed: reason')
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
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 () => {
const picker = await ResourcePicker.create(app, {
resourceType: 'product',
}).dispatch();
if (picker.cancelled || picker.selection.length === 0) return;
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.*.
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.
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.
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:
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_WRITE' (iframe), 'CLIPBOARD_READ_REQUEST'
Print, // 'PRINT'
Share, // 'SHARE'
History, // 'HISTORY_PUSH' | 'REPLACE' | 'BACK' | 'FORWARD' | 'GO'
Lifecycle, // 'LIFECYCLE_MOUNT', 'LIFECYCLE_UNMOUNT'
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';
Features.APP_BRIDGE_FEATURES (re-exported from the package root) is the
one named-constant the SDK ships — a string-literal union of every
feature flag a host may advertise via FEATURES_QUERY. Use it in
discriminated checks:
import { APP_BRIDGE_FEATURES } from '@launchmystore/app-bridge';
const { features } = await Features.create(app).query();
if (features.includes(APP_BRIDGE_FEATURES.RESOURCE_PICKER)) {
// Safe to call ResourcePicker
}
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 for
the recovery pattern.
See Also