Admin sections
A provider plugin can add its own screen to the KahunaCart admin. This page is for PHP and JavaScript developers building one.
What a section is
A section is a web component. The plugin registers some metadata and a path to a JS file; the admin fetches that file and mounts the custom element it defines. A PayPal plugin adds a "PayPal" tab with its credentials and a live webhook status; a shipping integration adds a rate-card editor.
The base plugin does not know what any section contains. It collects them, sorts them, and serves their code.
Register a section
Subscribe to onKahunaCartAdminSections and append your section to sections.
public static function getSubscribedEvents(): array
{
return [
'onKahunaCartAdminSections' => ['onKahunaCartAdminSections', 0],
];
}
public function onKahunaCartAdminSections(Event $event): void
{
$sections = $event['sections'];
$sections[] = [
'id' => 'paypal',
'label' => 'PayPal',
'icon' => 'CreditCard',
'position' => 200,
'script_path' => 'plugin://kahunacart-paypal/admin-next/sections/paypal.js',
];
$event['sections'] = $sections;
}
The section appears in the admin nav on the next request.
| Key | Required | Notes |
|---|---|---|
id |
yes | Lowercase, [a-z0-9][a-z0-9-]*, no underscores. It goes in a URL and in the custom element name, so anything else is dropped. First registration of an id wins; a later plugin cannot displace it. |
label |
no | Falls back to the id. |
icon |
no | A Lucide icon name, matching the rest of the admin. |
position |
no | Sort order, default 100. Ties break on label, so two plugins picking 100 stay in a stable order. |
script_path |
yes | Absolute path or Grav stream URI. Must resolve inside the plugins directory; anything else is refused. A section with no script is dropped. |
The event fires on demand, per request, when the admin asks for the list. There is nothing to invalidate and nothing to cache.
Section endpoints
GET /kahunacart/admin/sections → {"sections": [{id, label, icon, position}, …]}
GET /kahunacart/admin/sections/{id}.js → the component source, application/javascript
Both require the kahunacart.orders.view permission. script_path is a server-side detail and never appears in a response; the script is fetched by id instead. The script route answers a conditional GET with a 304, using mtime and size as the validator, so the admin can revalidate a fleet of sections cheaply.
The component contract
The script defines a custom element named kahunacart-section- plus the id.
// admin-next/sections/paypal.js
class KahunaCartPayPalSection extends HTMLElement {
connectedCallback() {
const { apiFetch, currency, exponent, route } = this.ctx;
// …render
}
}
customElements.define('kahunacart-section-paypal', KahunaCartPayPalSection);
The host instantiates the element and sets el.ctx before attaching it, so connectedCallback can rely on it.
ctx key |
What it is |
|---|---|
apiFetch |
(method, path, body) => Promise — an authenticated JSON fetch against the API. Pass paths like /kahunacart/coupons. It unwraps json.data and throws an Error carrying .status. |
apiBlob |
(path) => Promise<Blob> — the same authenticated fetch, returning a blob. |
currency |
The store's ISO 4217 code, for example USD. |
exponent |
How many decimal places that currency has: 2 for USD, 0 for JPY, 3 for KWD. |
base |
The API base URL the admin talks to, for example /api/v1. |
route |
The storefront's base route, for example /shop. |
providers |
slug => {label, capabilities} for every payment provider registered right now. |
languages |
The site's supported languages, in configured order. Empty on a single-language store. |
defaultLanguage |
The site's default language code, or null. |
version |
The KahunaCart version string. |
Important
Money crosses the API as minor units or as decimal strings. Use exponent for every conversion between the two. Hardcoding 2 breaks JPY and KWD.
Register the element name exactly once. customElements.define throws on a duplicate, and the host may load a section more than once across navigations.
if (!customElements.get('kahunacart-section-paypal')) {
customElements.define('kahunacart-section-paypal', KahunaCartPayPalSection);
}
Conventions to follow
- Put the file at
admin-next/sections/{id}.jsin your plugin. Nothing enforces it, but it is where the next person will look. - Ship one file with no imports. There is no bundler between your plugin and the browser; the route serves the bytes as they are on disk.
- Gate your own writes. The section renders for users with
kahunacart.orders.view, so if your screen changes settings, checkkahunacart.settingson your own endpoints rather than assuming a visible section means the user may save.