Developers
Start building
Apps/Admin

Admin

Where an app appears in the Poltura admin. Native pages and the Apps navigation, home cards, panels on product, collection, order and customer pages, worker settings, and hosted pages in an iframe.

On this page

An app can add four things to the admin: its own pages, a home card, panels on detail pages, and a hosted page you serve yourself. The first three are native workers; the last is an iframe.

Pages and navigation

A worker with the target admin.page becomes a page once it is listed in adminPages in the manifest, up to 8 of them. The admin shows them under Apps in its navigation, at /apps/<installation id>/<page id>. Without the list, the page exists in the release but nothing links to it.

useNavigate() moves between pages, with back and forward working as usual: { kind: 'app_page', pageId } opens another page of your app, and { kind: 'host', target } opens an admin screen, where target is app_home, products, collections, orders, customers, product, collection, order or customer.

The admin page of the complete example reads its settings, opens the product picker, shows toasts and calls an action:

export function AdminPage(): ReactNode {
  const copy = copyFor(useLocale());
  const settings = useSettings();
  const navigate = useNavigate();
  const toast = useToast();
  const pick = useResourcePicker();
  const saveFeatured = useAction('save_featured');
  const heading = String(settings.heading ?? copy.heading);
  const [picked, setPicked] = useState('');

  return (
    <Card className='offer-panel rounded-theme bg-surface text-foreground p-4'>
      <Stack>
        <Heading>{heading}</Heading>
        <Text>{copy.adminIntro}</Text>
        <TextField label={copy.featuredLabel} value={picked} onChange={setPicked} />
        <Inline>
          <Button
            label={copy.pickProducts}
            onPress={async () => {
              const ids = await pick('product', true);
              if (ids === null) return;
              setPicked(ids.join(','));
              await toast('success', copy.pickedToast);
            }}
          />
          <Button
            label={copy.save}
            onPress={async () => {
              const productIds = picked
                .split(',')
                .map((value) => value.trim())
                .filter((value) => value.length > 0);
              await saveFeatured({
                productIds,
                expectedVersion: 0,
                idempotencyKey: crypto.randomUUID(),
              });
              await toast('success', copy.savedToast);
            }}
          />
          <Button
            label={copy.openPage}
            onPress={() => navigate({ kind: 'app_page', pageId: RECOMMENDATIONS_PAGE })}
          />
        </Inline>
      </Stack>
    </Card>
  );
}
  • useSettings() returns the worker's settings for this installation. See Worker settings.
  • useResourcePicker() opens the admin's own product or collection picker: pick('product' | 'collection', multiple) returns the chosen ids, up to 50, or null when the merchant cancels. The call gives up after 15 seconds.
  • useToast() shows an admin toast: toast('info' | 'success' | 'warning' | 'critical', message), with a message of at most 200 characters.
  • useAction('save_featured') calls your server. A worker cannot fetch your server or any other address; Poltura sends a signed POST to /actions/save_featured on backendOrigin instead, and your server checks it:
const raw = await readBody(req);
const output = await actions.handle(raw, requestHeaders(req));
send(res, 200, JSON.stringify(output), 'application/json; charset=utf-8');

See Signed actions for the server side.

Home cards

A worker with the target admin.home draws a card on the admin home. It uses the same components and settings as a page, and has no selected product or order.

Panels on detail pages

Workers with the targets admin.product, admin.collection, admin.order and admin.customer appear on those detail pages. useSelectedEntity() gives the panel the record the merchant has open, and only that one: the hook takes no id. Reading an order needs orders.read and reading a customer customers.read.

export function AdminProduct(): ReactNode {
  const copy = copyFor(useLocale());
  const selected = useSelectedEntity();
  const navigate = useNavigate();
  if (selected.status !== 'ready' || selected.entity.kind !== 'product') {
    return <Text>{copy.unavailable}</Text>;
  }
  return (
    <Card className='rounded-theme bg-surface text-foreground p-4'>
      <Stack>
        <Heading>{selected.entity.product.title}</Heading>
        <Button
          label={copy.openSettings}
          onPress={() => navigate({ kind: 'app_page', pageId: RECOMMENDATIONS_PAGE })}
        />
      </Stack>
    </Card>
  );
}

useSelectedEntity() starts as loading and settles on ready or unavailable; show something useful for each. The native widgets example has an order panel and a product panel.

Worker settings

The admin generates a settings screen for each worker from the settings of its defineBlock definition (field types text, textarea, number, range, checkbox, select, radio, color and url). When the merchant saves, useSettings() returns the new values. A worker without a defineBlock definition has no settings. The manifest's own settings list is not shown anywhere yet (see Manifest).

Hosted iframe pages

A hosted page is a page on your own server, shown in an iframe in the admin. Use it when you need your own HTML, CSS and JavaScript. Declare it with adminFrame in the manifest: its exact HTTPS address and the bridge actions it may use, from toast, set_loading, navigate_host, pick_resource, resize, get_session_token and get_appearance.

The iframe is sandboxed with allow-scripts allow-forms allow-same-origin: the page cannot open pop-ups or navigate the admin. In the page, connectAppFrame from @poltura/app-sdk/frame connects to the admin with the same action list. To call your server, request a session token with requestAppSession and send it along; the installation id the admin passes to the page during the connection is for display only, not proof. The whole exchange is on Authentication. The complete example's server checks the token like this:

if (sessions === null) {
  send(res, 503, 'Session verification is not configured.');
  return;
}
const header = requestHeaders(req).get('authorization') ?? '';
const token = header.startsWith('Bearer ') ? header.slice('Bearer '.length) : '';
const assertion = await sessions.verify(token);
send(
  res,
  200,
  JSON.stringify({ installationId: assertion.installationId, storeId: assertion.storeId }),
  'application/json; charset=utf-8',
);

To draw with Poltura's components in a hosted page, import @poltura/app-ui/dom, request get_appearance and wrap the page in PolturaProvider. That entry point is for hosted admin pages only; it adds no isolation of its own.

Merchants open a hosted page from Connected apps (/connected-apps); it lives at /connected-apps/<installation id>/page.

Next

  • Storefront: blocks, header and footer zones, and storefront pages.
  • Authentication: tokens, session tokens and signed actions.
Last updated