OpenPOS Self-Checkout ([op_self_checkout] shortcode)
The self-checkout shortcode turns any page into a storefront where a customer browses products, builds a cart, and pays — inline, without ever leaving the page. It is designed for kiosks, in-store QR ordering, and restaurant tables.
Everything runs through the OpenPOS product formatter (get_product_formatted_data) so per-warehouse product data, quantities and in-store flags stay consistent with the rest of OpenPOS, and it exposes a full set of hooks so payment gateways (Stripe, PayPal, …) can be added without modifying the core plugin.
1. Using the shortcode
Drop the shortcode onto any page:
[op_self_checkout]
Attributes
| Attribute | Default | Description |
|---|---|---|
limit | 12 | Products loaded per AJAX page (infinite scroll). |
category | (empty) | Product category slug to pre-filter the list. |
columns | 4 | Product grid columns on desktop. |
mode | grocery | restaurant adds a Dine in / Take away choice at checkout. grocery omits it. |
warehouse_id | 0 | 0 = online store. A warehouse id loads that outlet’s product / quantity / in-store data. |
pin | (empty) | Access PIN (anti-spam). Empty = open access. When set, the page is locked until the PIN is entered. |
Examples
[op_self_checkout] → online store, grocery mode
[op_self_checkout mode="restaurant" warehouse_id="5"] → table ordering for outlet #5
[op_self_checkout columns="3" limit="18" category="drinks"] → 3-col grid, pre-filtered to "drinks"
[op_self_checkout pin="2468"] → gated behind a PIN
What the shopper gets
- AJAX product list with infinite scroll and a quick search box.
- Category bar on a single row — swipe left/right to see more.
- Variation / options dialog when a product has variations or add-ons.
- Floating cart button fixed to the bottom-right of the screen.
- Inline checkout dialog — name, email, phone, (restaurant) service type, and the payment method. Payment completes in a dialog; the page never redirects.
The PIN gate (anti-spam)
When pin is set, visitors see a lock screen and must enter the PIN before any action works. It is enforced on both the client and the server:
- The PIN is never printed to the page — only a salted
wp_hash()token is, which cannot be reversed without the site’s secret salts. - The PIN is verified server-side with a constant-time comparison and stored in the WooCommerce session.
- Every AJAX action is gated: a request that carries the page’s token but has not unlocked the session is refused with
code: pin_required. This stops scripted/bot order spam, not just the UI.
Note: a short numeric PIN can, in theory, be brute-forced online. Use it as an anti-spam speed bump, not as authentication for sensitive data.
2. Payment methods
Each payment method is a small descriptor:
array(
'code' => 'stripe', // unique id, also the WC gateway id if backed by one
'label' => 'Credit card', // shown to the shopper
'type' => 'event', // 'offline' | 'event' (see below)
'description' => 'Pay by card', // optional, shown under the label
)
By default every available WooCommerce gateway is offered as an offline method. Add-ons change that through filters.
type decides the checkout flow
type | What happens when the shopper clicks Place order |
|---|---|
offline | The order is created and the gateway’s process_payment() runs server-side immediately. Used by Cash on delivery, bank transfer, and any gateway that does not need a redirect. |
event | The order is saved as pending payment, then control is handed to a JavaScript payment handler through the self.checkout.payment.* events (inline Stripe / PayPal). |
Adding or changing methods — opsc_payment_methods
add_filter( 'opsc_payment_methods', function ( $methods, $context, $self ) {
// Make the Stripe gateway use the inline event flow.
if ( isset( $methods['stripe'] ) ) {
$methods['stripe']['type'] = 'event';
}
// Add a method that has no WooCommerce gateway behind it.
$methods['my_wallet'] = array(
'code' => 'my_wallet',
'label' => 'Store wallet',
'type' => 'event',
'description' => 'Pay from your prepaid balance',
);
// Hide a method.
unset( $methods['cheque'] );
return $methods;
}, 10, 3 );
$context is 'render' (building the dialog) or 'process' (placing the order). The same filtered list is used for both, so a method you add for display is also accepted on submit.
To add/remove/reorder the underlying WooCommerce gateway objects instead (e.g. inject a gateway that isn’t normally available on the front end), use opsc_payment_gateways( $gateways, $context, $self ).
Rendering inline fields — opsc_payment_method_fields
Print inline UI inside a method’s option — e.g. a Stripe card element mount point. Each method already has a stable container: #opsc-pay-fields-{code}.
add_action( 'opsc_payment_method_fields', function ( $code, $pm, $gw, $self ) {
if ( 'stripe' === $code ) {
echo '<div id="opsc-card-element"></div>';
}
}, 10, 4 );
Enqueue your JavaScript separately (see opsc_footer_assets) and bind it to that element.
3. Events
The event flow is the contract between the core self-checkout and a payment handler (your Stripe/PayPal add-on). All events are CustomEvents dispatched on window.
Core fires → your handler listens
| Event | When | detail payload |
|---|---|---|
self.checkout.payment.start | Shopper clicked Place order with an event method; the pending order is saved. | { order_id, order_key, number, code, amount, currency, order:{ id, number, email, first_name, phone, service_type }, payment:{…} } |
payment contains whatever your opsc_start_payment filter attached server-side (e.g. a Stripe client_secret).
Your handler fires → core listens
| Event | Fire it when | detail you send | Core’s reaction |
|---|---|---|---|
self.checkout.payment.success | Money captured. | { order_id, transaction_id } | Calls complete_payment → order paid → success dialog, cart cleared. |
self.checkout.payment.failed | Charge declined / errored. | { order_id, message } | Shows the message + a Retry button (re-fires payment.start). |
self.checkout.payment.cancel | Shopper closed your payment UI. | { order_id } | Calls cancel_payment → order cancelled → checkout dialog closed. |
Diagram
Place order (event method)
│
▼
op_sc_start_payment ──► order = pending
│
▼
window ⟶ self.checkout.payment.start { order_id, amount, order, payment }
│
[ your handler charges the card ]
│
┌────┼───────────────┬────────────────────┐
▼ ▼ ▼ ▼
success failed cancel
│ │ │
op_sc_complete Retry button op_sc_cancel
│ (re-start) │
order paid order cancelled
success dialog dialog closed
4. Hooking and firing events (client side)
A payment handler is just a script that listens for start, does the charge, and fires one of success / failed / cancel. Print it with opsc_footer_assets so it lands in the footer, outside the_content (safe from wptexturize).
add_action( 'opsc_footer_assets', function () {
?>
<script>
window.addEventListener('self.checkout.payment.start', function (e) {
var d = e.detail || {};
if ( d.code !== 'stripe' ) { return; } // only handle your method
// d.payment.client_secret came from your opsc_start_payment filter.
var clientSecret = d.payment.client_secret;
// ... run your SDK (Stripe.confirmCardPayment, PayPal buttons, …) ...
yourSdk.pay(clientSecret).then(function (res) {
if ( res.ok ) {
window.dispatchEvent(new CustomEvent('self.checkout.payment.success', {
detail: { order_id: d.order_id, transaction_id: res.id }
}));
} else if ( res.cancelled ) {
window.dispatchEvent(new CustomEvent('self.checkout.payment.cancel', {
detail: { order_id: d.order_id }
}));
} else {
window.dispatchEvent(new CustomEvent('self.checkout.payment.failed', {
detail: { order_id: d.order_id, message: res.error || 'Payment failed.' }
}));
}
});
});
</script>
<?php
} );
That is the entire client contract — you never touch the core JavaScript.
5. Server-side hooks reference
| Hook | Type | Signature | Purpose |
|---|---|---|---|
opsc_payment_gateways | filter | ($gateways, $context, $self) | Add / remove / reorder the WC gateway objects. |
opsc_payment_methods | filter | ($methods, $context, $self) | Method descriptors; set type, add custom methods. |
opsc_payment_method_fields | action | ($code, $pm, $gw, $self) | Print inline UI inside a method’s option. |
opsc_start_payment | filter | ($payment, $order, $gateway, $self) | Event flow: attach data for the client handler (e.g. a PaymentIntent client secret). Returned array becomes the payment object in the start event. |
opsc_process_payment | filter | ($result, $gateway, $order, $self) | Offline flow: return a WooCommerce result array to charge inline instead of the gateway’s process_payment(). |
opsc_complete_payment | filter | ($ok, $order, $self) | Event flow: verify the charge server-side before completing. Return WP_Error/false to reject. |
opsc_order_placed | action | ($order, $gateway, $self) | After an order is paid/placed (offline or event). |
opsc_order_cancelled | action | ($order, $self) | After an order is cancelled from the payment step. |
opsc_order_response | filter | ($response, $order, $self) | Adjust the JSON returned to the browser on success. |
opsc_footer_assets | action | ($self) | Print inline <script> in the footer (bind your SDK here). |
6. Complete example — an inline “event” payment add-on
This is a self-contained mini add-on: it registers an event method, attaches data on start, verifies on complete, and ships the client handler. Replace the simulated parts with your real payment SDK.
<?php
/**
* Plugin Name: OpenPOS Self-Checkout — Demo Event Gateway
*/
if ( ! defined( 'ABSPATH' ) ) { exit; }
// 1) Offer an event-type method.
add_filter( 'opsc_payment_methods', function ( $methods ) {
$methods['demo_pay'] = array(
'code' => 'demo_pay',
'label' => 'Demo card',
'type' => 'event',
'description' => 'Inline demo payment',
);
return $methods;
} );
// 2) When the order is saved (pending), create the payment intent and hand the
// client whatever it needs. This array becomes event.detail.payment.
add_filter( 'opsc_start_payment', function ( $payment, $order, $gateway, $self ) {
// $intent = MyApi::createIntent( $order->get_total(), $order->get_currency() );
$payment['client_secret'] = 'demo_secret_' . $order->get_id();
return $payment;
}, 10, 4 );
// 3) Verify server-side before the order is completed.
add_filter( 'opsc_complete_payment', function ( $ok, $order, $self ) {
// $intent = MyApi::retrieveIntent( ... );
// if ( $intent->status !== 'succeeded' ) return new WP_Error( 'unpaid', 'Not paid.' );
return true;
}, 10, 3 );
// 4) The client handler.
add_action( 'opsc_footer_assets', function () {
?>
<script>
window.addEventListener('self.checkout.payment.start', function (e) {
var d = e.detail || {};
if ( d.code !== 'demo_pay' ) { return; }
setTimeout(function () { // pretend to talk to the processor
window.dispatchEvent(new CustomEvent('self.checkout.payment.success', {
detail: { order_id: d.order_id, transaction_id: 'DEMO-' + d.order_id }
}));
}, 800);
});
</script>
<?php
} );
7. AJAX endpoints (for reference)
All are action=op_sc_<name> on admin-ajax.php, protected by the op_sc_nonce nonce and (when configured) the PIN gate.
verify_pin, add, get_cart, set_qty, remove, products, get_options, place_order (offline), start_payment / complete_payment / cancel_payment (event flow).
You normally never call these directly — the shortcode’s front-end script and the events above are the supported surface.