Hello,
What you describe is doable as a custom plugin, and HikaShop actually gives you two clean places to render the buttons on the payment step, so you do not have to fight the checkout. Here is the architecture I would consider safest, point by point.
Rendering the buttons on the payment step, two options:
1) Payment-plugin custom HTML (simplest, recommended to start). A payment plugin can output its own HTML in the payment block: implement onPaymentDisplay() and set $method->custom_html on your method. HikaShop renders that HTML under the method, and only when the method is selected, which is exactly where you want the buttons. Several gateways (Klarna, Alma, AfterPay, Atos...) already use this to inject their widgets at the payment step.
2) Checkout API custom block (more control over placement). If you would rather have the buttons as their own checkout block instead of tied to the payment method, register a block with the Checkout API: onCheckoutStepList() to declare it so it shows up in the checkout workflow configuration, onInitCheckoutStep() to prepare its data, and onCheckoutStepDisplay() to output its HTML (for layouts prefixed with "plg."). The merchant then drops your block wherever they want in the workflow.
Start with option 1 (scoped to the method, shown on selection automatically); use option 2 if you need the buttons outside the payment-method list.
createOrder, before any HikaShop order exists
Compute the amount server-side from the current HikaShop cart (cart and currency classes), never from the client, and create the PayPal order for that amount in an endpoint of your plugin. At the same time, store a fingerprint of the cart (a hash of the line items, total and currency) in the HikaShop checkout session. That is your cart snapshot, re-checked at capture.
Creating the HikaShop order in the approval callback
Yes, creating the order only in the PayPal approval callback is the right choice to avoid early orders. Create it by going through HikaShop's own checkout order-creation path (the same one the confirmation step uses), not a hand-rolled insert, so every validation and every other plugin hooking order creation still runs. Before creating it, re-read the cart and compare to the fingerprint: if anything changed (price, stock, coupon, contents), abort and send the customer back. That "pay, then the cart changes" window is exactly what the standard end-of-checkout flow avoids by design. Then capture the payment server-side, verify the captured amount equals the order total, and on success redirect to that order's standard end/thanks page.
Security points not to skip
All amounts and the capture verification stay server-side; the client only carries the PayPal order id. You are effectively re-implementing the guarantees of HikaShop's confirmation step (terms acceptance, final stock/coupon/total validation, totals lock), and anything missed there becomes a consistency or security gap. Reuse the PayPal Checkout plugin's existing API class for create/capture instead of rewriting the PayPal calls; it already handles auth and the REST endpoints.
On performance, the step stays light: the SDK and the createOrder call only fire when your method is selected, and createOrder is a single server call. The heavy work (validation, order creation, capture) happens once, on approval, so the page itself does not get slower.
Skeleton to point you in the right direction (option 1, illustrative, not copy-paste ready):
// plugins/plg_hikashoppayment_paypalinline/paypalinline.php
class plgHikashoppaymentPaypalinline extends hikashopPaymentPlugin
{
var $name = 'paypalinline';
// 1) Render the buttons on the payment step (shown only when this method is selected)
public function onPaymentDisplay(&$order, &$methods, &$usable_methods)
{
parent::onPaymentDisplay($order, $methods, $usable_methods);
foreach ($methods as $method) {
if ($method->payment_type != $this->name || !$method->enabled) continue;
$clientId = $method->payment_params->client_id;
$createUrl = hikashop_completeLink('checkout&task=notify¬if_payment=' . $this->name . '&action=create', true);
$captureUrl = hikashop_completeLink('checkout&task=notify¬if_payment=' . $this->name . '&action=capture', true);
$method->custom_html =
'<div id="paypal-inline"></div>'
. '<script src="https://www.paypal.com/sdk/js?client-id=' . htmlspecialchars($clientId) . '&components=buttons&intent=capture"></script>'
. '<script>paypal.Buttons({'
. ' createOrder: function(){ return fetch("' . $createUrl . '", {method:"POST"}).then(r=>r.json()).then(d=>d.id); },'
. ' onApprove: function(data){ return fetch("' . $captureUrl . '", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({orderID:data.orderID})})'
. ' .then(r=>r.json()).then(function(res){ if(res.redirect) window.location = res.redirect; }); }'
. '}).render("#paypal-inline");</script>';
}
return true;
}
// 2) Both AJAX actions arrive on the plugin's notify URL (same channel as the IPN)
public function onPaymentNotification(&$statuses)
{
$action = hikaInput::get()->getCmd('action');
$api = $this->getPaypalApi(); // reuse the PayPal Checkout plugin's API class
if ($action == 'create') {
// amount computed SERVER-SIDE from the current cart, never from the client
$cart = hikashop_get('class.cart')->loadFullCart();
$total = $cart->full_total->prices[0]->price_value_with_tax;
$currency = $cart->full_total->prices[0]->price_currency_id;
$this->storeCartFingerprint($cart); // keep a snapshot in the checkout session
$ppOrder = $api->createOrder($total, $currency);
echo json_encode(array('id' => $ppOrder->id));
exit;
}
if ($action == 'capture') {
$in = json_decode(file_get_contents('php://input'));
// 1. re-read the cart and verify it still matches the stored fingerprint, else abort
// 2. create the HikaShop order via HikaShop's checkout order-creation path (so all plugins/validations run)
// 3. capture the PayPal payment server-side
$capture = $api->captureOrder($in->orderID);
// 4. verify $capture amount == order total, set the order status to paid
echo json_encode(array('redirect' => $thanksUrl));
exit;
}
}
}
Net: plan for a fully custom plugin; the pieces you can safely reuse are the existing plugin's PayPal API class and HikaShop's standard order-creation path. If you would rather not re-implement the confirmation-step guarantees, the lower-risk alternative is to keep the standard flow and condense the checkout so the buttons appear as early as possible.