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, ornullwhen 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 signedPOSTto/actions/save_featuredonbackendOrigininstead, 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.