Skip to content

Powered by Grav

Coming soon — KahunaCart is in final testing. Join the list and be first to know. Join the list

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.

PHP
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

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

JS
// 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.

JS
if (!customElements.get('kahunacart-section-paypal')) {
    customElements.define('kahunacart-section-paypal', KahunaCartPayPalSection);
}

Conventions to follow

  • Put the file at admin-next/sections/{id}.js in 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, check kahunacart.settings on your own endpoints rather than assuming a visible section means the user may save.