Files
2026-10-19 15:22:52 -07:00

113 lines
4.4 KiB
PHP

<?php
/**
* Embeddable payment frame (the cross-site "Pay with Square" surface).
* ────────────────────────────────────────────────────────────────────
* This page is loaded INSIDE an <iframe> on a school's own external website by
* embed.js. It resolves the school from its public embed key, renders the
* existing Web Payments widget, and auto-reports its height to the parent page
* so the iframe can size itself with no scrollbars.
*
* Query params:
* key (required) the school's public merchant_key (sqpk_…)
* amount (required) major-units amount, e.g. 25.00
* currency (optional) ISO-4217, default the school's onboarding currency/CAD
* email (optional) buyer email for the receipt
* success (optional) URL to redirect to after a successful charge
*
* Because this is meant to be framed, it deliberately does NOT send
* X-Frame-Options. When enforce_embed_origins is on and the school has
* registered origins, we additionally restrict framing via CSP frame-ancestors.
*/
require_once __DIR__ . '/lib/SquareClient.php';
require_once __DIR__ . '/lib/MerchantStore.php';
$config = require __DIR__ . '/config.php';
$store = new MerchantStore($config);
$key = isset($_GET['key']) ? trim((string)$_GET['key']) : '';
$amount = isset($_GET['amount']) && is_numeric($_GET['amount']) ? number_format((float)$_GET['amount'], 2, '.', '') : '';
$currency = isset($_GET['currency']) ? strtoupper(preg_replace('/[^A-Za-z]/', '', (string)$_GET['currency'])) : '';
$email = isset($_GET['email']) ? trim((string)$_GET['email']) : '';
$success = isset($_GET['success']) ? trim((string)$_GET['success']) : '';
if (!preg_match('#^(https?://|/)#i', $success)) {
$success = ''; // only real web URLs (blocks javascript:/data: redirects)
}
$school = $store->findSchoolByKey($key);
// Restrict who may frame us when origin enforcement is enabled.
if (!empty($config['enforce_embed_origins']) && $school !== null) {
$allowed = $store->allowedOrigins($school);
if (!empty($allowed)) {
header("Content-Security-Policy: frame-ancestors 'self' " . implode(' ', $allowed));
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pay</title>
<style>
html, body { margin:0; padding:0; background:transparent; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; }
.sq-frame-pad { padding:2px; }
</style>
</head>
<body>
<div class="sq-frame-pad">
<?php
if ($school === null) {
echo '<div style="background:#fff5f5;border:1px solid #feb2b2;color:#c53030;'
. 'border-radius:10px;padding:14px;font-size:13px;">'
. 'This payment button is not configured. (Unknown or removed key.)</div>';
} elseif ($amount === '') {
echo '<div style="background:#fff5f5;border:1px solid #feb2b2;color:#c53030;'
. 'border-radius:10px;padding:14px;font-size:13px;">Invalid payment amount.</div>';
} else {
$sqCheckout = [
'school_name' => $school,
'merchant_key' => $key,
'amount' => $amount,
'currency' => $currency ?: '', // widget falls back to onboarding currency/CAD
'order_api_url' => './order-api.php',
'success_url' => $success,
'buyer_email' => $email,
];
require __DIR__ . '/square-checkout-widget.php';
}
?>
</div>
<script>
// Report our content height to the embedding page so embed.js can size the
// iframe. Posts on load, on resize, and whenever the widget mutates (e.g. a
// status message appears). The parent matches on the {sqPayFrame:true} marker.
(function () {
function postHeight() {
var h = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight
);
try {
window.parent.postMessage({ sqPayFrame: true, type: 'height', height: h }, '*');
} catch (e) {}
}
window.addEventListener('load', postHeight);
window.addEventListener('resize', postHeight);
if (window.MutationObserver) {
new MutationObserver(postHeight).observe(document.body, {
childList: true, subtree: true, attributes: true, characterData: true
});
}
// A couple of delayed posts catch the async SDK card-form mount.
setTimeout(postHeight, 600);
setTimeout(postHeight, 1500);
}());
</script>
</body>
</html>