586 lines
24 KiB
PHP
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)
|
|
. '¤cy=' . $_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> & <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; ?>
|