Write a plugin
A plugin adds things to the store without touching its code: listening to what happens, changing what is calculated, putting a screen in the panel.
A plugin adds things to the store without touching its code: listening to what happens, changing what is calculated, putting a screen in the panel.
Getting started
npx pcc-plugin nuevo my-plugin
cd my-plugin && npm install && npm run devnpm run dev loads the plugin and loads it again every time you save a file.
It loads it outside the store, on its own bus: it is there to show that it
starts, that its hooks exist and that it declares the permissions they need.
Real events reach it once you install it.
The three commands
| Command | What it does |
|---|---|
pcc-plugin nuevo <name> | Creates the folder with plugin.json, src/index.js, package.json, jsconfig.json, README.md and .gitignore. The name is lowercase letters, digits and hyphens, and the folder must not exist. |
pcc-plugin comprobar [dir] | Validates plugin.json, checks that entry exists and reports every error at once. If it is fine, it shows what the plugin will be able to do and flags the sensitive permissions. |
pcc-plugin dev [dir] | Loads it in development mode and reloads it on save. Ctrl+C to quit. |
Without [dir] the current folder is used. The plugin nuevo creates already
has npm run dev and npm run comprobar pointing at those two.
The whole plugin
import { definirPlugin } from "@pcreative/plugin-contract"
export default definirPlugin({
arrancar(api, { registro }) {
api.on("pedido.creado", (pedido) => {
registro.info(`Order ${pedido.number}: ${pedido.total.amount} €`)
})
},
})Type api.on(" and the editor shows you the available events, already typed. It
is the only thing you need to know to start.
Events and filters
An event tells you about something that has already happened. You cannot change the result:
api.on("cliente.registrado", (cliente) => { /* welcome them */ })A filter lets you modify something before it is used:
api.filtro("envios.disponibles", (metodos, { carrito }) => {
return metodos.filter((m) => m.id !== "urgente")
})The hooks that exist today:
| Events | Filters |
|---|---|
pedido.creado, pedido.pagado | precio.mostrado |
pedido.enviado, pedido.cancelado | producto.ficha |
producto.creado, producto.actualizado | envios.disponibles |
cliente.registrado | pagos.disponibles |
inventario.agotado, carrito.abandonado | correo.contenido |
factura.emitida | impuestos.calculados |
A filter can change what is shown, but never what is charged:
producto.fichachanges text, but not prices, stock or variants.envios.disponiblesandpagos.disponiblesremove, reorder or rename options, but do not change their price or surcharge.
To change a price there is precio.mostrado, which applies the same on the
product page, in the cart and at checkout.
carrito.totales is still accepted but no longer applied: it changed the totals
shown without changing what is charged or invoiced. If your plugin declares it,
you get a warning when you check it.
Taxes and e-invoicing
These two hooks are what a tax connector uses (Stripe Tax, Avalara, each country's e-invoicing):
impuestos.calculadosdoes change what is charged, which is why it needs theimpuestos:escribirpermission, a sensitive one. It receives the taxes of every line and shipping method ({ lines: { [id]: [{ name, code, rate, compound }] }, shipping: {…} }) plus the cart destination, and returns the same shape with the right rates. If it returns something malformed, fails or takes more than 5 seconds, the store's own rates stay: checkout is never blocked by an outside service.factura.emitidaarrives with every numbered invoice and credit note: issuer, customer, lines, per-rate breakdown and the chain hash. Withtienda.facturas.sellar(serie, numero, { provider, status, reference, url, qr })(permissionfacturas:escribir) the connector leaves its receipt, and the stamp is printed on the invoice, with its QR code.
Besides events and filters, a plugin can add UI to the panel at
menu.principal, pedido.lateral, producto.lateral, ajustes.pestaña and
inicio.tarjeta.
Three kinds of extension, one of them works
plugin.json says what kind of extension it is, in kind. The contract accepts
three, and only one runs today:
kind | Where it would run | Today |
|---|---|---|
plugin | In your store, loaded by the backend from entry (JavaScript). | Works. It is the only kind the loader loads. pcc-plugin nuevo creates this one. |
app | On its developer's server: it has no entry, it declares a webhookUrl, and its panel contributions need a url. | Not implemented. comprobar validates it, but the store does not load it and sends it nothing. |
sandbox | In your store, in an isolated space with permissions granted one by one. Its entry must be a .wasm file. | Not implemented. Same: it is validated and not loaded. |
If you put an app or a sandbox in the plugins folder, the backend refuses it
at start-up and says why: "this loader only loads plugins".
Installing it
Copy the plugin folder into plugins/ at the root of the repository (next to
apps/, not inside apps/backend). In Docker the folder is /plugins inside
the backend container. It appears in Panel → Extensions.
If your plugins live somewhere else, say so with PLUGINS_DIR.
To turn one off without uninstalling it:
PLUGINS_DISABLED=one,anotherHandy when a plugin is causing trouble and you want to keep selling while you look at it.
Settings
If your plugin declares settings, they appear by themselves in Panel → Extensions: you do not have to write the screen. Secrets are stored apart and not returned to the browser — the panel shows "a key is stored", never the key.
Paid plugins
A paid plugin declares a license block in plugin.json, and the store checks
it before loading the plugin. It is verified against a public key
(LICENSE_PUBLIC_KEY), and verified offline: a store that loses its internet
cannot lose its plugins. If the licence is not valid, the plugin does not load;
there is no half-working mode.
You do not need a licence while you build it: pcc-plugin dev loads in
development mode, which skips the check and warns about it on every load.
That mode is chosen by whoever runs the loader, never by the plugin: nothing you
put in plugin.json makes a store skip the check.
The key is public on purpose: it can only check signatures, never create them.
The contract does not depend on the inside
On purpose. A plugin written today should keep working if the store changes
underneath tomorrow. That is why the events talk about pedido and carrito,
not the engine's tables.
See also
- Selling the theme you built — packaging, the marketplace and licences; it covers extensions too
- The extension contract on npm (
@pcreative/plugin-contract) - The commands