Files
HotLunches/pp-integration/paypal-checkout-widget.php
2026-10-19 15:22:52 -07:00

586 lines
24 KiB
PHP

<?php
/**
* PayPal Checkout Widget
* ---------------------
* Drop-in include for any school payment page.
* Renders PayPal buttons FIRST, above all other payment options.
*
* Usage — set $ppCheckout before including this file:
*
* $ppCheckout = [
* 'school_name' => 'Springfield Elementary', // required
* 'amount' => '25.00', // required (string)
* 'currency' => 'USD', // optional, default USD
* 'items' => [ // optional line items
* ['name' => 'Lunch x5', 'quantity' => '5',
* 'unit_amount' => ['currency_code' => 'USD', 'value' => '5.00']],
* ],
* 'buyer_email' => 'parent@school.com', // optional — prefills PayPal login
* 'page_type' => 'checkout', // checkout | product-detail | cart
* 'order_api_url' => '/sm/order-api.php', // URL to order-api.php
* 'success_url' => '/payment-success', // redirect after capture
* 'cancel_url' => '', // optional — redirect on cancel
* ];
* require_once '/path/to/ppp/paypal-checkout-widget.php';
*
* After the widget, add your other payment options (card, etc.) — they will appear below the divider.
*/
if (!isset($ppCheckout) || !is_array($ppCheckout)) {
throw new RuntimeException('$ppCheckout array must be set before including paypal-checkout-widget.php');
}
// -- Resolve merchant ID -------------------------------------------------------
$_ppConfig = require __DIR__ . '/config.php';
$_ppMerchantId = null;
$_ppSchoolReady = true;
$_ppMerchantsFile = __DIR__ . '/data/school_merchants.json';
if (file_exists($_ppMerchantsFile)) {
$_ppMerchants = json_decode(file_get_contents($_ppMerchantsFile), true) ?: [];
$_ppRecord = $_ppMerchants[$ppCheckout['school_name']] ?? null;
if ($_ppRecord) {
$_ppMerchantId = $_ppRecord['merchant_id'] ?? null;
if (isset($_ppRecord['paypal_ready'])) {
$_ppSchoolReady = (bool)$_ppRecord['paypal_ready'];
}
}
}
// Silently skip rendering if school is not connected or not ready
if (!$_ppMerchantId || !$_ppSchoolReady) {
return;
}
// -- FraudNet correlation ID (one per session) ---------------------------------
if (session_status() !== PHP_SESSION_ACTIVE) {
// Session started by the main app — we just read from it
}
if (empty($_SESSION['pp_fraudnet_id'])) {
$_SESSION['pp_fraudnet_id'] = bin2hex(random_bytes(16));
}
$_ppFraudnetId = $_SESSION['pp_fraudnet_id'];
// -- Params --------------------------------------------------------------------
$_ppClientId = $_ppConfig['client_id'];
$_ppCurrency = strtoupper($ppCheckout['currency'] ?? 'USD');
$_ppPageType = $ppCheckout['page_type'] ?? 'checkout';
// PayPal's SDK only accepts a fixed set of data-page-type values; an unknown
// value makes the SDK throw "Invalid page type" and no buttons render. Map
// common aliases and fall back to 'checkout' for anything unrecognised.
$_ppValidPageTypes = [
'home', 'category', 'product-listing', 'search-results', 'product-details',
'cart', 'mini-cart', 'checkout', 'order-review', 'order-confirmation',
'account', 'profile', 'blog', 'about', 'contact', 'landing', 'marketing',
'help', 'search', 'unknown',
];
$_ppPageTypeAliases = ['product-detail' => 'product-details', 'product' => 'product-details'];
$_ppPageType = $_ppPageTypeAliases[$_ppPageType] ?? $_ppPageType;
if (!in_array($_ppPageType, $_ppValidPageTypes, true)) {
$_ppPageType = 'checkout';
}
$_ppApiUrl = $ppCheckout['order_api_url'] ?? './order-api.php';
$_ppSuccessUrl = $ppCheckout['success_url'] ?? '';
$_ppCancelUrl = $ppCheckout['cancel_url'] ?? '';
$_ppAmount = $ppCheckout['amount'];
$_ppItems = json_encode($ppCheckout['items'] ?? [], JSON_UNESCAPED_UNICODE);
$_ppSchool = $ppCheckout['school_name'];
$_ppEmail = $ppCheckout['buyer_email'] ?? '';
// Optional — forwarded to order-api.php on capture so the payment can be booked
// to the buyer's account. Defaults to 0 (e.g. demos) where there's no app user.
$_ppUserNum = (int)($ppCheckout['user_num'] ?? 0);
$_ppEnv = strtolower($_ppConfig['env'] ?? 'sandbox');
// Pay Later promo messaging — the SDK's messaging component has thrown
// unhandled "PAGE_TYPE" exceptions in some environments. Set
// 'paylater_messaging' => false in config.php to exclude the component and
// its render entirely (buttons/wallets are unaffected).
// Defaults to FALSE: the SDK's messaging component throws an unhandled
// "Cannot set properties of undefined (setting 'PAGE_TYPE')" that aborts the
// SDK onload handler and stops the wallet buttons initialising. Opt in
// explicitly with 'paylater_messaging' => true once that SDK bug is resolved —
// a config file that predates this key must not silently re-enable the crash.
$_ppMessagesOn = (bool)($_ppConfig['paylater_messaging'] ?? false);
// -- JS SDK URL ----------------------------------------------------------------
$_ppSdkUrl = 'https://www.paypal.com/sdk/js'
. '?client-id=' . urlencode($_ppClientId)
. '&merchant-id=' . urlencode($_ppMerchantId)
. '&currency=' . $_ppCurrency
. '&intent=capture'
. '&commit=true'
. '&enable-funding=venmo,paylater'
. '&components=buttons' . ($_ppMessagesOn ? ',messages' : '') . ',applepay,googlepay';
// -- Unique widget ID (allows multiple widgets on same page) -------------------
// NOTE: no hyphens — this id is interpolated into the pp_initButtons_<id>
// JavaScript function name, and hyphens would make that an invalid identifier
// (JS would read them as minus operators ? SyntaxError). It's also used for DOM
// ids / CSS selectors, where an alphanumeric id is perfectly valid.
$_ppWidgetId = 'ppwidget' . substr(md5($_ppSchool . $_ppAmount), 0, 8);
?>
<?php /* -- FraudNet (invisible, must load before buttons) -- */ ?>
<script type="application/json" fncls="fnparams-dede7cc5-15fd-4c75-a9f4-36c430ee3a99">
{"f":"<?= htmlspecialchars($_ppFraudnetId) ?>","s":"HOTLUNCHES_CHECKOUT"}
</script>
<script type="text/javascript" src="https://c.paypal.com/da/r/fb.js"></script>
<?php /* -- Widget HTML -- */ ?>
<div id="<?= $_ppWidgetId ?>" class="pp-checkout-wrap">
<?php /* Acceptance banner — "We accept PayPal and Venmo" (checklist row 12) */ ?>
<div class="pp-accept-banner">
<svg class="pp-icon-paypal" viewBox="0 0 101 32" xmlns="http://www.w3.org/2000/svg" aria-label="PayPal">
<path fill="#003087" d="M12.237 2.179H5.771C5.338 2.179 4.97 2.49 4.903 2.918L2.179 20.165c-.05.314.193.599.511.599h3.07c.433 0 .801-.311.868-.739l.695-4.407c.066-.428.434-.739.867-.739h2.049c4.266 0 6.729-2.063 7.376-6.152.29-1.788.012-3.192-.818-4.176-.913-1.079-2.532-1.372-4.56-1.372zm.748 6.061c-.354 2.322-2.132 2.322-3.851 2.322h-.978l.686-4.343c.041-.261.267-.453.531-.453h.449c1.17 0 2.275 0 2.845.667.341.399.445.99.318 1.807z"/>
<path fill="#009cde" d="M33.952 8.18h-3.08c-.265 0-.491.192-.531.453l-.137.867-.217-.314c-.671-.974-2.167-1.299-3.661-1.299-3.425 0-6.348 2.596-6.918 6.234-.296 1.815.125 3.549 1.148 4.757 1.027 1.218 2.493 1.727 4.237 1.727 2.762 0 4.294-1.776 4.294-1.776l-.138.861c-.05.314.194.599.512.599h2.773c.434 0 .802-.311.868-.739l1.664-10.57c.051-.314-.193-.6-.814-.8zm-4.299 6.032c-.299 1.773-1.707 2.964-3.501 2.964-.899 0-1.619-.289-2.081-.836-.458-.543-.631-1.316-.487-2.176.279-1.757 1.706-2.986 3.474-2.986.88 0 1.595.291 2.066.841.473.554.661 1.33.529 2.193z"/>
</svg>
<span>We accept</span>
<strong>PayPal</strong> &amp; <strong>Venmo</strong>
</div>
<?php /* Pay Later message — promotional text above the buttons (placement: 'payment') */ ?>
<div id="<?= $_ppWidgetId ?>-message" class="pp-paylater-message"></div>
<?php /* PayPal buttons — rendered FIRST before all other payment options */ ?>
<div class="pp-buttons-section">
<p class="pp-section-label">Fastest checkout</p>
<?php /* Apple Pay / Google Pay — hidden until the SDK confirms buyer + merchant eligibility */ ?>
<div id="<?= $_ppWidgetId ?>-applepay" class="pp-wallet-btn pp-applepay-btn" style="display:none;"></div>
<div id="<?= $_ppWidgetId ?>-googlepay" class="pp-wallet-btn pp-googlepay-btn" style="display:none;"></div>
<div id="<?= $_ppWidgetId ?>-buttons" class="pp-buttons-container"></div>
<div id="<?= $_ppWidgetId ?>-error" class="pp-error-msg" style="display:none;"></div>
<div id="<?= $_ppWidgetId ?>-loading" class="pp-loading">
<span class="pp-spinner"></span> Loading payment options…
</div>
</div>
<?php /* Divider before other payment methods */ ?>
<div class="pp-divider"><span>or pay another way</span></div>
</div><?php /* end widget */ ?>
<?php /* -- Scoped styles -- */ ?>
<style>
#<?= $_ppWidgetId ?> {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
width: 100%;
max-width: 500px;
}
/* Acceptance banner */
.pp-accept-banner {
display: flex;
align-items: center;
gap: 6px;
background: #f5f7fa;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 8px 14px;
font-size: 13px;
color: #444;
margin-bottom: 16px;
}
.pp-accept-banner strong { color: #003087; }
.pp-icon-paypal { height: 18px; width: auto; }
/* Buttons section */
.pp-buttons-section {
background: #fff;
border: 1.5px solid #e2e8f0;
border-radius: 10px;
padding: 18px 16px 14px;
}
.pp-section-label {
font-size: 12px;
font-weight: 600;
color: #718096;
text-transform: uppercase;
letter-spacing: .5px;
margin: 0 0 12px;
}
.pp-buttons-container { min-height: 45px; }
/* Apple Pay / Google Pay wallet buttons */
.pp-wallet-btn { margin-bottom: 10px; min-height: 45px; }
.pp-wallet-btn > * { width: 100%; }
.pp-applepay-btn apple-pay-button {
--apple-pay-button-width: 100%;
--apple-pay-button-height: 45px;
--apple-pay-button-border-radius: 8px;
display: block;
}
.pp-googlepay-btn .gpay-card-info-container,
.pp-googlepay-btn button { width: 100% !important; border-radius: 8px !important; }
/* Loading skeleton */
.pp-loading {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #718096;
padding: 10px 0;
}
.pp-spinner {
display: inline-block;
width: 16px; height: 16px;
border: 2px solid #e2e8f0;
border-top-color: #003087;
border-radius: 50%;
animation: pp-spin .7s linear infinite;
}
@keyframes pp-spin { to { transform: rotate(360deg); } }
/* Error message */
.pp-error-msg {
background: #fff5f5;
border: 1px solid #feb2b2;
color: #c53030;
border-radius: 8px;
padding: 10px 14px;
font-size: 13px;
margin-top: 12px;
}
/* Divider */
.pp-divider {
display: flex;
align-items: center;
gap: 12px;
margin: 20px 0;
color: #a0aec0;
font-size: 13px;
}
.pp-divider::before,
.pp-divider::after {
content: '';
flex: 1;
height: 1px;
background: #e2e8f0;
}
/* Pay Later message */
.pp-paylater-message {
min-height: 20px;
margin-bottom: 10px;
font-size: 13px;
}
</style>
<?php /* -- PayPal JS SDK (one load per page via global flag) -- */ ?>
<?php
$_ppSdkAlreadyLoaded = $GLOBALS['_pp_sdk_loaded'] ?? false;
if (!$_ppSdkAlreadyLoaded) { $GLOBALS['_pp_sdk_loaded'] = true; }
?>
<?php
/* Wallet SDKs (Apple Pay web component + Google Pay client) load once whenever the
* checkout widget is on the page — independent of which include loaded the PayPal SDK,
* since the PayPal SDK may have been loaded earlier by the Pay Later message. */
$_ppWalletSdkLoaded = $GLOBALS['_pp_wallet_sdk_loaded'] ?? false;
if (!$_ppWalletSdkLoaded) { $GLOBALS['_pp_wallet_sdk_loaded'] = true; }
?>
<?php if (!$_ppWalletSdkLoaded): ?>
<script src="https://applepay.cdn-apple.com/jsapi/v1.1.0/apple-pay-sdk.js"></script>
<script src="https://pay.google.com/gp/p/js/pay.js"></script>
<?php endif; ?>
<?php /* -- Button initialisation -- */ ?>
<script>
(function () {
var WIDGET_ID = <?= json_encode($_ppWidgetId) ?>;
var SCHOOL = <?= json_encode($_ppSchool) ?>;
var AMOUNT = <?= json_encode($_ppAmount) ?>;
var CURRENCY = <?= json_encode($_ppCurrency) ?>;
var ITEMS = <?= $_ppItems ?>;
var BUYER_EMAIL = <?= json_encode($_ppEmail) ?>;
var USER_NUM = <?= json_encode((string)$_ppUserNum) ?>;
var FRAUDNET_ID = <?= json_encode($_ppFraudnetId) ?>;
var API_URL = <?= json_encode($_ppApiUrl) ?>;
var SUCCESS_URL = <?= json_encode($_ppSuccessUrl) ?>;
var CANCEL_URL = <?= json_encode($_ppCancelUrl) ?>;
var GP_ENV = <?= json_encode($_ppEnv === 'live' ? 'PRODUCTION' : 'TEST') ?>;
var $loading = document.getElementById(WIDGET_ID + '-loading');
var $error = document.getElementById(WIDGET_ID + '-error');
var $buttons = document.getElementById(WIDGET_ID + '-buttons');
function showError(msg) {
if ($error) { $error.style.display = 'block'; $error.textContent = msg; }
if ($loading) $loading.style.display = 'none';
}
function post(body) {
return fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(function (r) { return r.json(); });
}
window['pp_sdkError_' + WIDGET_ID] = function () {
showError('Could not load PayPal. Please refresh the page or try another payment method.');
};
// -- Shared order helpers (used by PayPal, Apple Pay and Google Pay) --
function createOrder() {
if ($error) $error.style.display = 'none';
return post({
action: 'create_order',
school_name: SCHOOL,
amount: AMOUNT,
currency: CURRENCY,
items: ITEMS,
buyer_email: BUYER_EMAIL,
fraudnet_id: FRAUDNET_ID,
}).then(function (d) {
if (!d.success) throw new Error(d.error || 'Order creation failed');
return d.order_id;
});
}
function captureOrder(orderId) {
return post({
action: 'capture_order',
order_id: orderId,
school_name: SCHOOL,
user_num: USER_NUM,
}).then(function (d) {
if (!d.success) throw new Error(d.error || 'Payment could not be completed');
if (SUCCESS_URL) {
var sep = SUCCESS_URL.indexOf('?') === -1 ? '?' : '&';
window.location.href = SUCCESS_URL + sep + 'capture_id=' + encodeURIComponent(d.capture_id || '');
}
return d;
});
}
window['pp_initButtons_' + WIDGET_ID] = function () {
if ($loading) $loading.style.display = 'none';
var paypalNS = window.paypal;
if (!paypalNS || !paypalNS.Buttons) {
showError('PayPal failed to initialise. Please refresh the page.');
return;
}
paypalNS.Buttons({
style: {
layout: 'vertical',
color: 'gold',
shape: 'rect',
label: 'pay',
},
// -- Create order -------------------------------------------
createOrder: function () {
return createOrder();
},
// -- Approve ? capture ---------------------------------------
onApprove: function (data) {
return captureOrder(data.orderID);
},
// -- Cancel -------------------------------------------------
onCancel: function () {
if (CANCEL_URL) window.location.href = CANCEL_URL;
},
// -- Error --------------------------------------------------
onError: function (err) {
console.error('PayPal error:', err);
showError('Something went wrong with PayPal. Please try again or use another payment method.');
},
}).render('#' + WIDGET_ID + '-buttons');
// Pay Later messaging — renders inline promotional text (e.g. "Pay in 4 payments of $X").
// No `placement`/`pageType` params: the SDK's internal placement?PAGE_TYPE mapping
// has crashed with "Cannot set properties of undefined (setting 'PAGE_TYPE')";
// with only amount + style the component picks sane defaults.
<?php if ($_ppMessagesOn): ?>
if (paypalNS.Messages) {
try {
paypalNS.Messages({
amount: parseFloat(AMOUNT),
style: { layout: 'text', logo: { type: 'inline' }, text: { size: 13 } },
}).render('#' + WIDGET_ID + '-message');
} catch (e) {
console.warn('Pay Later message skipped:', e);
}
}
<?php endif; ?>
// Wallet buttons — each self-hides if buyer device or merchant isn't eligible.
// Wrapped individually: a throw inside either init (e.g. the SDK's messaging
// component raising "Cannot set properties of undefined (setting 'PAGE_TYPE')")
// must not abort this handler, or the other wallet — and any code after it —
// silently never runs. PayPal/card buttons render above and are unaffected.
try { initApplePay(paypalNS); } catch (e) { console.warn('Apple Pay init skipped:', e); }
try { initGooglePay(paypalNS); } catch (e) { console.warn('Google Pay init skipped:', e); }
};
// -- Apple Pay --------------------------------------------------
// Eligibility: Safari + a device with Apple Pay, served over HTTPS on a
// domain registered with PayPal, and the merchant granted the APPLE_PAY
// capability. If any of these fail the button simply never shows.
function initApplePay(paypalNS) {
// Diagnostics — open the browser console to see which gate hides the button.
if (!paypalNS.Applepay) {
console.warn('[ApplePay] hidden: SDK applepay component not loaded (paypal.Applepay missing).');
return;
}
if (!window.ApplePaySession) {
console.warn('[ApplePay] hidden: not Safari / no Apple Pay on this device (window.ApplePaySession undefined). Apple Pay only renders in Safari on Apple hardware.');
return;
}
if (!ApplePaySession.canMakePayments()) {
console.warn('[ApplePay] hidden: ApplePaySession.canMakePayments() is false (no card in Wallet, or device cannot pay).');
return;
}
var applepay = paypalNS.Applepay();
applepay.config().then(function (cfg) {
console.log('[ApplePay] config:', cfg);
if (!cfg || !cfg.isEligible) {
console.warn('[ApplePay] hidden: merchant not eligible (isEligible=false). Merchant lacks the APPLE_PAY capability, or the domain is not registered/verified with PayPal.');
return;
}
var $ap = document.getElementById(WIDGET_ID + '-applepay');
$ap.innerHTML = '<apple-pay-button buttonstyle="black" type="buy" locale="en-US"></apple-pay-button>';
$ap.style.display = 'block';
$ap.querySelector('apple-pay-button').addEventListener('click', function () {
var paymentRequest = {
countryCode: cfg.countryCode,
currencyCode: CURRENCY,
merchantCapabilities: cfg.merchantCapabilities,
supportedNetworks: cfg.supportedNetworks,
requiredBillingContactFields: ['name', 'phone', 'email', 'postalAddress'],
total: { label: SCHOOL, amount: AMOUNT, type: 'final' },
};
var session = new ApplePaySession(4, paymentRequest);
session.onvalidatemerchant = function (event) {
applepay.validateMerchant({ validationUrl: event.validationURL })
.then(function (payload) { session.completeMerchantValidation(payload.merchantSession); })
.catch(function (err) { console.error('Apple Pay validateMerchant:', err); session.abort(); });
};
session.onpaymentauthorized = function (event) {
createOrder().then(function (orderId) {
return applepay.confirmOrder({
orderId: orderId,
token: event.payment.token,
billingContact: event.payment.billingContact,
}).then(function () {
session.completePayment(ApplePaySession.STATUS_SUCCESS);
return captureOrder(orderId);
});
}).catch(function (err) {
console.error('Apple Pay error:', err);
session.completePayment(ApplePaySession.STATUS_FAILURE);
showError('Apple Pay payment could not be completed. Please try another method.');
});
};
session.begin();
});
}).catch(function (err) { console.error('Apple Pay config:', err); });
}
// -- Google Pay -------------------------------------------------
// Eligibility: Google Pay JS loaded, merchant granted the GOOGLE_PAY
// capability, and isReadyToPay() true for this buyer. Self-hides otherwise.
function initGooglePay(paypalNS) {
if (!paypalNS.Googlepay) return;
if (!window.google || !google.payments || !google.payments.api) return;
var googlepay = paypalNS.Googlepay();
googlepay.config().then(function (cfg) {
if (!cfg || !cfg.isEligible) return;
var paymentsClient = new google.payments.api.PaymentsClient({ environment: GP_ENV });
paymentsClient.isReadyToPay({
apiVersion: cfg.apiVersion,
apiVersionMinor: cfg.apiVersionMinor,
allowedPaymentMethods: cfg.allowedPaymentMethods,
}).then(function (res) {
if (!res || !res.result) return;
var $gp = document.getElementById(WIDGET_ID + '-googlepay');
var btn = paymentsClient.createButton({
onClick: onGooglePayClicked,
buttonType: 'pay',
buttonSizeMode: 'fill',
});
$gp.appendChild(btn);
$gp.style.display = 'block';
}).catch(function (err) { console.error('Google Pay isReadyToPay:', err); });
function onGooglePayClicked() {
var paymentDataRequest = {
apiVersion: cfg.apiVersion,
apiVersionMinor: cfg.apiVersionMinor,
allowedPaymentMethods: cfg.allowedPaymentMethods,
merchantInfo: cfg.merchantInfo,
transactionInfo: {
countryCode: cfg.countryCode || 'US',
currencyCode: CURRENCY,
totalPriceStatus: 'FINAL',
totalPrice: AMOUNT,
},
};
paymentsClient.loadPaymentData(paymentDataRequest).then(function (paymentData) {
return createOrder().then(function (orderId) {
return googlepay.confirmOrder({
orderId: orderId,
paymentMethodData: paymentData.paymentMethodData,
}).then(function (confirm) {
if (confirm.status === 'APPROVED' || confirm.status === 'PAYER_ACTION_REQUIRED') {
return captureOrder(orderId);
}
throw new Error('Google Pay not approved: ' + confirm.status);
});
});
}).catch(function (err) {
if (err && err.statusCode === 'CANCELED') return; // buyer closed the sheet
console.error('Google Pay error:', err);
showError('Google Pay payment could not be completed. Please try another method.');
});
}
}).catch(function (err) { console.error('Google Pay config:', err); });
}
}());
</script>
<?php /* -- PayPal JS SDK — loaded AFTER the init/handler definitions above.
A script element's load event can fire before later inline scripts
execute, so putting the SDK tag first raced its onload against the
pp_initButtons_<id> definition (ReferenceError, widget stuck on
"Loading payment options…"). Emitting the SDK tag last guarantees
the handlers exist before onload/onerror can possibly fire. -- */ ?>
<?php if (!$_ppSdkAlreadyLoaded): ?>
<?php /* NOTE: no data-page-type attribute — the SDK has an internal bug where
that attribute makes it throw "Cannot set properties of undefined
(setting 'PAGE_TYPE')" asynchronously after load, which can break the
Pay Later message and wallet eligibility checks. It is an optional
analytics hint only; buttons and messages work fine without it. */ ?>
<script
src="<?= htmlspecialchars($_ppSdkUrl) ?>"
data-partner-attribution-id="EZSSOFTWAREINC_SP_PPCP"
onload="pp_initButtons_<?= $_ppWidgetId ?>()"
onerror="pp_sdkError_<?= $_ppWidgetId ?>()"
></script>
<?php else: ?>
<script>
// SDK was loaded earlier on this page — call init directly (function is defined above)
pp_initButtons_<?= $_ppWidgetId ?>();
</script>
<?php endif; ?>