Looking for help?
< All Topics
Print

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

AttributeDefaultDescription
limit12Products loaded per AJAX page (infinite scroll).
category(empty)Product category slug to pre-filter the list.
columns4Product grid columns on desktop.
modegroceryrestaurant adds a Dine in / Take away choice at checkout. grocery omits it.
warehouse_id00 = 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

typeWhat happens when the shopper clicks Place order
offlineThe 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.
eventThe 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

EventWhendetail payload
self.checkout.payment.startShopper 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

EventFire it whendetail you sendCore’s reaction
self.checkout.payment.successMoney captured.{ order_id, transaction_id }Calls complete_payment → order paid → success dialog, cart cleared.
self.checkout.payment.failedCharge declined / errored.{ order_id, message }Shows the message + a Retry button (re-fires payment.start).
self.checkout.payment.cancelShopper 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

HookTypeSignaturePurpose
opsc_payment_gatewaysfilter($gateways, $context, $self)Add / remove / reorder the WC gateway objects.
opsc_payment_methodsfilter($methods, $context, $self)Method descriptors; set type, add custom methods.
opsc_payment_method_fieldsaction($code, $pm, $gw, $self)Print inline UI inside a method’s option.
opsc_start_paymentfilter($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_paymentfilter($result, $gateway, $order, $self)Offline flow: return a WooCommerce result array to charge inline instead of the gateway’s process_payment().
opsc_complete_paymentfilter($ok, $order, $self)Event flow: verify the charge server-side before completing. Return WP_Error/false to reject.
opsc_order_placedaction($order, $gateway, $self)After an order is paid/placed (offline or event).
opsc_order_cancelledaction($order, $self)After an order is cancelled from the payment step.
opsc_order_responsefilter($response, $order, $self)Adjust the JSON returned to the browser on success.
opsc_footer_assetsaction($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.

Table of Contents