Release v1.5.1

This commit is contained in:
AudioCore Bot
2026-04-01 14:12:17 +00:00
parent dc53051358
commit 9deabe1ec9
50 changed files with 10775 additions and 5637 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -181,11 +181,15 @@ return function (Router $router): void {
$router->post('/cart/discount/apply', [$controller, 'cartApplyDiscount']);
$router->post('/cart/discount/remove', [$controller, 'cartClearDiscount']);
$router->get('/checkout', [$controller, 'checkoutIndex']);
$router->post('/checkout/card/start', [$controller, 'checkoutCardStart']);
$router->get('/checkout/card', [$controller, 'checkoutCard']);
$router->get('/account', [$controller, 'accountIndex']);
$router->post('/account/request-login', [$controller, 'accountRequestLogin']);
$router->get('/account/login', [$controller, 'accountLogin']);
$router->get('/account/logout', [$controller, 'accountLogout']);
$router->post('/checkout/place', [$controller, 'checkoutPlace']);
$router->post('/checkout/paypal/create-order', [$controller, 'checkoutPaypalCreateOrder']);
$router->post('/checkout/paypal/capture-order', [$controller, 'checkoutPaypalCaptureJson']);
$router->get('/checkout/paypal/return', [$controller, 'checkoutPaypalReturn']);
$router->get('/checkout/paypal/cancel', [$controller, 'checkoutPaypalCancel']);
$router->post('/checkout/sandbox', [$controller, 'checkoutSandbox']);
@@ -199,6 +203,8 @@ return function (Router $router): void {
$router->post('/admin/store/settings/rebuild-sales-chart', [$controller, 'adminRebuildSalesChart']);
$router->post('/admin/store/discounts/create', [$controller, 'adminDiscountCreate']);
$router->post('/admin/store/discounts/delete', [$controller, 'adminDiscountDelete']);
$router->post('/admin/store/bundles/create', [$controller, 'adminBundleCreate']);
$router->post('/admin/store/bundles/delete', [$controller, 'adminBundleDelete']);
$router->post('/admin/store/settings/test-email', [$controller, 'adminSendTestEmail']);
$router->post('/admin/store/settings/test-paypal', [$controller, 'adminTestPaypal']);
$router->get('/admin/store/customers', [$controller, 'adminCustomers']);

View File

@@ -1,8 +1,11 @@
<?php
use Core\Services\Plugins;
$pageTitle = $title ?? 'Store Customers';
$customers = $customers ?? [];
$currency = (string)($currency ?? 'GBP');
$q = (string)($q ?? '');
$reportsEnabled = Plugins::isEnabled('advanced-reporting');
ob_start();
?>
<section class="admin-card customers-page">
@@ -20,6 +23,9 @@ ob_start();
<a href="/admin/store/settings" class="btn outline small">Settings</a>
<a href="/admin/store/orders" class="btn outline small">Orders</a>
<a href="/admin/store/customers" class="btn outline small">Customers</a>
<?php if ($reportsEnabled): ?>
<a href="/admin/store/reports" class="btn outline small">Sales Reports</a>
<?php endif; ?>
</div>
<form method="get" action="/admin/store/customers" class="customers-search">
@@ -39,7 +45,9 @@ ob_start();
<tr>
<th>Customer</th>
<th>Orders</th>
<th>Revenue</th>
<th>Before Fees</th>
<th>Fees</th>
<th>After Fees</th>
<th>Latest Order</th>
<th>Last Seen</th>
</tr>
@@ -70,7 +78,9 @@ ob_start();
<?php endif; ?>
</td>
<td class="num"><?= (int)($customer['order_count'] ?? 0) ?></td>
<td class="num"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($customer['revenue'] ?? 0), 2) ?></td>
<td class="num"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($customer['before_fees'] ?? 0), 2) ?></td>
<td class="num"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($customer['paypal_fees'] ?? 0), 2) ?></td>
<td class="num"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($customer['after_fees'] ?? 0), 2) ?></td>
<td>
<?php if ($lastOrderId > 0): ?>
<a href="/admin/store/order?id=<?= $lastOrderId ?>" class="order-link">
@@ -206,8 +216,10 @@ ob_start();
@media (max-width: 980px) {
.customers-table th:nth-child(3),
.customers-table td:nth-child(3),
.customers-table th:nth-child(5),
.customers-table td:nth-child(5) { display:none; }
.customers-table th:nth-child(4),
.customers-table td:nth-child(4),
.customers-table th:nth-child(7),
.customers-table td:nth-child(7) { display:none; }
}
@media (max-width: 700px) {

View File

@@ -1,4 +1,6 @@
<?php
use Core\Services\Plugins;
$pageTitle = $title ?? 'Store';
$tablesReady = (bool)($tables_ready ?? false);
$privateRoot = (string)($private_root ?? '');
@@ -9,8 +11,11 @@ $newCustomers = is_array($new_customers ?? null) ? $new_customers : [];
$currency = (string)($currency ?? 'GBP');
$totalOrders = (int)($stats['total_orders'] ?? 0);
$paidOrders = (int)($stats['paid_orders'] ?? 0);
$totalRevenue = (float)($stats['total_revenue'] ?? 0);
$beforeFees = (float)($stats['before_fees'] ?? 0);
$paypalFees = (float)($stats['paypal_fees'] ?? 0);
$afterFees = (float)($stats['after_fees'] ?? 0);
$totalCustomers = (int)($stats['total_customers'] ?? 0);
$reportsEnabled = Plugins::isEnabled('advanced-reporting');
ob_start();
?>
<section class="admin-card">
@@ -24,6 +29,9 @@ ob_start();
<a href="/admin/store/settings" class="btn outline">Settings</a>
<a href="/admin/store/orders" class="btn outline">Orders</a>
<a href="/admin/store/customers" class="btn outline">Customers</a>
<?php if ($reportsEnabled): ?>
<a href="/admin/store/reports" class="btn outline">Sales Reports</a>
<?php endif; ?>
</div>
</div>
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:12px;">
@@ -31,6 +39,9 @@ ob_start();
<a href="/admin/store/settings" class="btn outline small">Settings</a>
<a href="/admin/store/orders" class="btn outline small">Orders</a>
<a href="/admin/store/customers" class="btn outline small">Customers</a>
<?php if ($reportsEnabled): ?>
<a href="/admin/store/reports" class="btn outline small">Sales Reports</a>
<?php endif; ?>
</div>
<?php if (!$tablesReady): ?>
@@ -62,8 +73,14 @@ ob_start();
<div style="font-size:12px; color:var(--muted); margin-top:4px;">Paid: <?= $paidOrders ?></div>
</div>
<div class="admin-card" style="padding:14px;">
<div class="label">Revenue</div>
<div style="font-size:26px; font-weight:700; margin-top:8px;"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($totalRevenue, 2) ?></div>
<div class="label">Before Fees</div>
<div style="font-size:26px; font-weight:700; margin-top:8px;"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($beforeFees, 2) ?></div>
<div style="font-size:12px; color:var(--muted); margin-top:4px;">Gross paid sales</div>
</div>
<div class="admin-card" style="padding:14px;">
<div class="label">After Fees</div>
<div style="font-size:26px; font-weight:700; margin-top:8px;"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($afterFees, 2) ?></div>
<div style="font-size:12px; color:var(--muted); margin-top:4px;">PayPal fees: <?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($paypalFees, 2) ?></div>
</div>
<div class="admin-card" style="padding:14px;">
<div class="label">Total Customers</div>
@@ -86,7 +103,11 @@ ob_start();
</div>
<div style="display:flex; justify-content:space-between; gap:10px; color:var(--muted); font-size:12px; margin-top:4px;">
<span><?= htmlspecialchars((string)($order['email'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
<span><?= htmlspecialchars((string)($order['currency'] ?? $currency), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['total'] ?? 0), 2) ?></span>
<span><?= htmlspecialchars((string)($order['currency'] ?? $currency), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['payment_net'] ?? $order['total'] ?? 0), 2) ?></span>
</div>
<div style="display:flex; justify-content:space-between; gap:10px; color:var(--muted); font-size:12px; margin-top:4px;">
<span>Before fees <?= htmlspecialchars((string)($order['currency'] ?? $currency), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['payment_gross'] ?? $order['total'] ?? 0), 2) ?></span>
<span>Fees <?= htmlspecialchars((string)($order['currency'] ?? $currency), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['payment_fee'] ?? 0), 2) ?></span>
</div>
<div style="margin-top:8px;">
<a href="/admin/store/order?id=<?= (int)($order['id'] ?? 0) ?>" class="btn outline small">View Order</a>

View File

@@ -1,9 +1,12 @@
<?php
use Core\Services\Plugins;
$pageTitle = $title ?? 'Order Detail';
$order = is_array($order ?? null) ? $order : [];
$items = is_array($items ?? null) ? $items : [];
$downloadsByItem = is_array($downloads_by_item ?? null) ? $downloads_by_item : [];
$downloadEvents = is_array($download_events ?? null) ? $download_events : [];
$reportsEnabled = Plugins::isEnabled('advanced-reporting');
ob_start();
?>
<section class="admin-card">
@@ -20,33 +23,45 @@ ob_start();
<a href="/admin/store/settings" class="btn outline small">Settings</a>
<a href="/admin/store/orders" class="btn outline small">Orders</a>
<a href="/admin/store/customers" class="btn outline small">Customers</a>
<?php if ($reportsEnabled): ?>
<a href="/admin/store/reports" class="btn outline small">Sales Reports</a>
<?php endif; ?>
</div>
<div class="admin-card" style="padding:14px; margin-top:16px;">
<div style="display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px;">
<div>
<div class="admin-card order-summary-card" style="margin-top:16px;">
<div class="order-summary-top">
<div class="order-summary-identity">
<div class="label">Order Number</div>
<div style="font-weight:700; margin-top:6px;"><?= htmlspecialchars((string)($order['order_no'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
<div class="order-summary-no"><?= htmlspecialchars((string)($order['order_no'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
<div class="order-summary-meta">Customer <?= htmlspecialchars((string)($order['email'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
</div>
<div>
<div class="label">Status</div>
<div style="font-weight:700; margin-top:6px;"><?= htmlspecialchars((string)($order['status'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
<div class="order-summary-total-cluster">
<div class="order-summary-top-stat">
<div class="label">Status</div>
<div class="pill"><?= htmlspecialchars((string)($order['status'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
</div>
<div class="order-summary-top-stat">
<div class="label">After Fees</div>
<div class="order-summary-total-value"><?= htmlspecialchars((string)($order['payment_currency'] ?? $order['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['payment_net'] ?? $order['total'] ?? 0), 2) ?></div>
</div>
</div>
<div>
<div class="label">Total</div>
<div style="font-weight:700; margin-top:6px;"><?= htmlspecialchars((string)($order['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['total'] ?? 0), 2) ?></div>
</div>
<div class="order-summary-grid">
<div class="order-stat">
<div class="label">Before Fees</div>
<div class="order-stat-value"><?= htmlspecialchars((string)($order['payment_currency'] ?? $order['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['payment_gross'] ?? $order['total'] ?? 0), 2) ?></div>
</div>
<div>
<div class="label">Customer Email</div>
<div style="margin-top:6px;"><?= htmlspecialchars((string)($order['email'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
<div class="order-stat">
<div class="label">PayPal Fees</div>
<div class="order-stat-value"><?= htmlspecialchars((string)($order['payment_currency'] ?? $order['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['payment_fee'] ?? 0), 2) ?></div>
</div>
<div>
<div class="order-stat">
<div class="label">Order IP</div>
<div style="margin-top:6px;"><?= htmlspecialchars((string)($order['customer_ip'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
<div class="order-stat-value order-stat-text"><?= htmlspecialchars((string)($order['customer_ip'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
</div>
<div>
<div class="order-stat">
<div class="label">Created</div>
<div style="margin-top:6px;"><?= htmlspecialchars((string)($order['created_at'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
<div class="order-stat-value order-stat-text"><?= htmlspecialchars((string)($order['created_at'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
</div>
</div>
</div>
@@ -118,6 +133,108 @@ ob_start();
<?php endif; ?>
</div>
</section>
<style>
.order-summary-card {
padding: 18px;
display: grid;
gap: 18px;
}
.order-summary-top {
display: grid;
grid-template-columns: minmax(0, 1.6fr) auto;
gap: 20px;
align-items: start;
padding-bottom: 18px;
border-bottom: 1px solid rgba(255,255,255,.08);
}
.order-summary-identity {
min-width: 0;
}
.order-summary-no {
margin-top: 8px;
font-size: 18px;
font-weight: 700;
line-height: 1.2;
font-family: 'IBM Plex Mono', monospace;
letter-spacing: .02em;
color: #f3f6ff;
word-break: break-all;
}
.order-summary-meta {
margin-top: 10px;
color: var(--muted);
font-size: 13px;
line-height: 1.4;
word-break: break-word;
}
.order-summary-total-cluster {
display: grid;
gap: 10px;
min-width: 220px;
}
.order-summary-top-stat {
display: grid;
gap: 6px;
justify-items: end;
text-align: right;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,.08);
background: rgba(255,255,255,.025);
}
.order-summary-total-value {
font-size: 28px;
font-weight: 700;
line-height: 1;
white-space: nowrap;
}
.order-summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.order-stat {
padding: 14px 16px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,.08);
background: rgba(255,255,255,.025);
min-width: 0;
}
.order-stat-value {
margin-top: 8px;
font-size: 22px;
font-weight: 700;
line-height: 1.1;
}
.order-stat-text {
font-size: 16px;
font-weight: 600;
color: #eef2ff;
word-break: break-word;
}
@media (max-width: 900px) {
.order-summary-top {
grid-template-columns: 1fr;
}
.order-summary-total-cluster {
grid-template-columns: 1fr 1fr;
min-width: 0;
}
.order-summary-top-stat {
justify-items: start;
text-align: left;
}
.order-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.order-summary-total-cluster,
.order-summary-grid {
grid-template-columns: 1fr;
}
}
</style>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../../modules/admin/views/layout.php';

View File

@@ -1,9 +1,12 @@
<?php
use Core\Services\Plugins;
$pageTitle = $title ?? 'Store Orders';
$orders = is_array($orders ?? null) ? $orders : [];
$q = (string)($q ?? '');
$saved = (string)($saved ?? '');
$error = (string)($error ?? '');
$reportsEnabled = Plugins::isEnabled('advanced-reporting');
ob_start();
?>
<section class="admin-card store-orders">
@@ -21,6 +24,9 @@ ob_start();
<a href="/admin/store/settings" class="btn outline small">Settings</a>
<a href="/admin/store/orders" class="btn small">Orders</a>
<a href="/admin/store/customers" class="btn outline small">Customers</a>
<?php if ($reportsEnabled): ?>
<a href="/admin/store/reports" class="btn outline small">Sales Reports</a>
<?php endif; ?>
</div>
<?php if ($saved !== ''): ?>
@@ -78,7 +84,11 @@ ob_start();
<div class="store-order-amount">
<?= htmlspecialchars((string)($order['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?>
<?= number_format((float)($order['total'] ?? 0), 2) ?>
<?= number_format((float)($order['payment_net'] ?? $order['total'] ?? 0), 2) ?>
<div style="margin-top:6px; color:var(--muted); font-size:12px; font-weight:500;">
Before fees <?= htmlspecialchars((string)($order['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['payment_gross'] ?? $order['total'] ?? 0), 2) ?>
· Fees <?= htmlspecialchars((string)($order['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['payment_fee'] ?? 0), 2) ?>
</div>
</div>
<div class="store-order-status pill"><?= htmlspecialchars($status, ENT_QUOTES, 'UTF-8') ?></div>

View File

@@ -1,35 +1,51 @@
<?php
$pageTitle = $title ?? 'Store Settings';
$settings = $settings ?? [];
<?php
use Core\Services\Plugins;
$pageTitle = $title ?? 'Store Settings';
$settings = $settings ?? [];
$gateways = is_array($gateways ?? null) ? $gateways : [];
$error = (string)($error ?? '');
$saved = (string)($saved ?? '');
$tab = (string)($tab ?? 'general');
$tab = in_array($tab, ['general', 'payments', 'emails', 'discounts', 'sales_chart'], true) ? $tab : 'general';
$tab = in_array($tab, ['general', 'payments', 'emails', 'discounts', 'bundles', 'sales_chart'], true) ? $tab : 'general';
$paypalTest = (string)($_GET['paypal_test'] ?? '');
$privateRootReady = (bool)($private_root_ready ?? false);
$discounts = is_array($discounts ?? null) ? $discounts : [];
$bundles = is_array($bundles ?? null) ? $bundles : [];
$bundleReleaseOptions = is_array($bundle_release_options ?? null) ? $bundle_release_options : [];
$chartRows = is_array($chart_rows ?? null) ? $chart_rows : [];
$chartLastRebuildAt = (string)($chart_last_rebuild_at ?? '');
$chartCronUrl = (string)($chart_cron_url ?? '');
$chartCronCmd = (string)($chart_cron_cmd ?? '');
ob_start();
?>
$reportsEnabled = Plugins::isEnabled('advanced-reporting');
ob_start();
?>
<section class="admin-card">
<div class="badge">Store</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
<div>
<h1 style="font-size:28px; margin:0;">Store Settings</h1>
<p style="color: var(--muted); margin-top:6px;">Configure defaults, payments, and transactional emails.</p>
</div>
<a href="/admin/store" class="btn outline">Back</a>
</div>
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:12px;">
<a href="/admin/store/settings?tab=general" class="btn <?= $tab === 'general' ? '' : 'outline' ?> small">General</a>
<a href="/admin/store" class="btn outline">Back</a>
</div>
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:12px;">
<a href="/admin/store" class="btn outline small">Overview</a>
<a href="/admin/store/settings" class="btn small">Settings</a>
<a href="/admin/store/orders" class="btn outline small">Orders</a>
<a href="/admin/store/customers" class="btn outline small">Customers</a>
<?php if ($reportsEnabled): ?>
<a href="/admin/store/reports" class="btn outline small">Sales Reports</a>
<?php endif; ?>
</div>
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:12px;">
<a href="/admin/store/settings?tab=general" class="btn <?= $tab === 'general' ? '' : 'outline' ?> small">General</a>
<a href="/admin/store/settings?tab=payments" class="btn <?= $tab === 'payments' ? '' : 'outline' ?> small">Payments</a>
<a href="/admin/store/settings?tab=emails" class="btn <?= $tab === 'emails' ? '' : 'outline' ?> small">Emails</a>
<a href="/admin/store/settings?tab=discounts" class="btn <?= $tab === 'discounts' ? '' : 'outline' ?> small">Discounts</a>
<a href="/admin/store/settings?tab=bundles" class="btn <?= $tab === 'bundles' ? '' : 'outline' ?> small">Bundles</a>
<a href="/admin/store/settings?tab=sales_chart" class="btn <?= $tab === 'sales_chart' ? '' : 'outline' ?> small">Sales Chart</a>
</div>
@@ -91,10 +107,10 @@ ob_start();
<button class="btn" type="submit">Save General Settings</button>
</div>
</form>
<?php elseif ($tab === 'payments'): ?>
<form method="post" action="/admin/store/settings" style="margin-top:16px; display:grid; gap:16px;">
<input type="hidden" name="tab" value="payments">
<div class="admin-card" style="padding:16px;">
<?php elseif ($tab === 'payments'): ?>
<form method="post" action="/admin/store/settings" style="margin-top:16px; display:grid; gap:16px;">
<input type="hidden" name="tab" value="payments">
<div class="admin-card" style="padding:16px;">
<div class="label" style="margin-bottom:10px;">Payment Mode</div>
<input type="hidden" name="store_test_mode" value="0">
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
@@ -103,22 +119,78 @@ ob_start();
</label>
</div>
<div class="admin-card" style="padding:16px;">
<div class="label" style="margin-bottom:10px;">PayPal</div>
<input type="hidden" name="store_paypal_enabled" value="0">
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
<input type="checkbox" name="store_paypal_enabled" value="1" <?= ((string)($settings['store_paypal_enabled'] ?? '0') === '1') ? 'checked' : '' ?>>
Enable PayPal
</label>
<div class="label" style="margin-top:10px;">PayPal Client ID</div>
<input class="input" name="store_paypal_client_id" value="<?= htmlspecialchars((string)($settings['store_paypal_client_id'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
<div class="label" style="margin-top:10px;">PayPal Secret</div>
<input class="input" name="store_paypal_secret" value="<?= htmlspecialchars((string)($settings['store_paypal_secret'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
<div style="margin-top:12px; display:flex; gap:8px; flex-wrap:wrap;">
<button class="btn outline small" type="submit" name="paypal_probe_mode" value="live" formaction="/admin/store/settings/test-paypal" formmethod="post">Test PayPal Live</button>
<button class="btn outline small" type="submit" name="paypal_probe_mode" value="sandbox" formaction="/admin/store/settings/test-paypal" formmethod="post">Test PayPal Sandbox</button>
<button class="btn" type="submit">Save Payment Settings</button>
<div class="admin-card" style="padding:16px;">
<div class="label" style="margin-bottom:10px;">PayPal</div>
<input type="hidden" name="store_paypal_enabled" value="0">
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
<input type="checkbox" name="store_paypal_enabled" value="1" <?= ((string)($settings['store_paypal_enabled'] ?? '0') === '1') ? 'checked' : '' ?>>
Enable PayPal
</label>
<div class="label" style="margin-top:10px;">PayPal Client ID</div>
<input class="input" name="store_paypal_client_id" value="<?= htmlspecialchars((string)($settings['store_paypal_client_id'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
<div class="label" style="margin-top:10px;">PayPal Secret</div>
<input class="input" name="store_paypal_secret" value="<?= htmlspecialchars((string)($settings['store_paypal_secret'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
<div style="display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; margin-top:12px;">
<div>
<div class="label">Merchant Country</div>
<input class="input" name="store_paypal_merchant_country" value="<?= htmlspecialchars((string)($settings['store_paypal_merchant_country'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="GB">
</div>
<div>
<div class="label">Card Button Label</div>
<input class="input" name="store_paypal_card_branding_text" value="<?= htmlspecialchars((string)($settings['store_paypal_card_branding_text'] ?? 'Pay with card'), ENT_QUOTES, 'UTF-8') ?>" placeholder="Pay with card">
</div>
</div>
<div style="display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; margin-top:12px;">
<div>
<div class="label">Card Checkout Mode</div>
<select class="input" name="store_paypal_sdk_mode">
<?php $sdkMode = (string)($settings['store_paypal_sdk_mode'] ?? 'embedded_fields'); ?>
<option value="embedded_fields" <?= $sdkMode === 'embedded_fields' ? 'selected' : '' ?>>Embedded card fields</option>
<option value="paypal_only_fallback" <?= $sdkMode === 'paypal_only_fallback' ? 'selected' : '' ?>>PayPal-only fallback</option>
</select>
</div>
<div style="display:flex; align-items:end;">
<div style="padding:12px 14px; border:1px solid rgba(255,255,255,.08); border-radius:12px; background:rgba(255,255,255,.03); width:100%;">
<input type="hidden" name="store_paypal_cards_enabled" value="0">
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
<input type="checkbox" name="store_paypal_cards_enabled" value="1" <?= ((string)($settings['store_paypal_cards_enabled'] ?? '0') === '1') ? 'checked' : '' ?>>
Enable Credit / Debit Card Checkout
</label>
</div>
</div>
</div>
<?php
$capabilityStatus = (string)($settings['store_paypal_cards_capability_status'] ?? 'unknown');
$capabilityMessage = (string)($settings['store_paypal_cards_capability_message'] ?? 'Run a PayPal credentials test to check card-field support.');
$capabilityCheckedAt = (string)($settings['store_paypal_cards_capability_checked_at'] ?? '');
$capabilityMode = (string)($settings['store_paypal_cards_capability_mode'] ?? '');
$capabilityColor = '#c7cfdf';
if ($capabilityStatus === 'available') {
$capabilityColor = '#9be7c6';
} elseif ($capabilityStatus === 'unavailable') {
$capabilityColor = '#f3b0b0';
}
?>
<div style="margin-top:12px; padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.03); display:grid; gap:6px;">
<div class="label" style="font-size:10px;">Card Capability</div>
<div style="font-weight:700; color:<?= htmlspecialchars($capabilityColor, ENT_QUOTES, 'UTF-8') ?>; text-transform:uppercase; letter-spacing:.12em;">
<?= htmlspecialchars($capabilityStatus !== '' ? $capabilityStatus : 'unknown', ENT_QUOTES, 'UTF-8') ?>
</div>
<div style="font-size:13px; color:var(--muted);"><?= htmlspecialchars($capabilityMessage, ENT_QUOTES, 'UTF-8') ?></div>
<?php if ($capabilityCheckedAt !== ''): ?>
<div style="font-size:12px; color:var(--muted);">
Last checked: <?= htmlspecialchars($capabilityCheckedAt, ENT_QUOTES, 'UTF-8') ?><?= $capabilityMode !== '' ? ' (' . htmlspecialchars($capabilityMode, ENT_QUOTES, 'UTF-8') . ')' : '' ?>
</div>
<?php endif; ?>
</div>
<div style="margin-top:12px; display:flex; gap:8px; flex-wrap:wrap;">
<button class="btn outline small" type="submit" name="paypal_probe_mode" value="live" formaction="/admin/store/settings/test-paypal" formmethod="post">Test PayPal Live</button>
<button class="btn outline small" type="submit" name="paypal_probe_mode" value="sandbox" formaction="/admin/store/settings/test-paypal" formmethod="post">Test PayPal Sandbox</button>
<button class="btn" type="submit">Save Payment Settings</button>
</div>
</div>
@@ -230,6 +302,89 @@ ob_start();
</div>
<?php endif; ?>
</div>
<?php elseif ($tab === 'bundles'): ?>
<div class="admin-card" style="margin-top:16px; padding:16px;">
<div class="label" style="margin-bottom:10px;">Create Bundle</div>
<form method="post" action="/admin/store/bundles/create" style="display:grid; gap:12px;">
<div style="display:grid; grid-template-columns:1.3fr .8fr .7fr .6fr; gap:10px;">
<div>
<div class="label" style="font-size:10px;">Bundle Name</div>
<input class="input" name="name" placeholder="Hard Dance Essentials" required>
</div>
<div>
<div class="label" style="font-size:10px;">Slug (optional)</div>
<input class="input" name="slug" placeholder="hard-dance-essentials">
</div>
<div>
<div class="label" style="font-size:10px;">Bundle Price</div>
<input class="input" name="bundle_price" value="9.99" required>
</div>
<div>
<div class="label" style="font-size:10px;">Currency</div>
<input class="input" name="currency" value="<?= htmlspecialchars((string)($settings['store_currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?>" maxlength="3">
</div>
</div>
<div style="display:grid; grid-template-columns:1fr auto; gap:10px; align-items:end;">
<div>
<div class="label" style="font-size:10px;">Button Label (optional)</div>
<input class="input" name="purchase_label" placeholder="Buy Discography">
</div>
<label style="display:flex; align-items:center; gap:6px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:.16em; padding-bottom:6px;">
<input type="checkbox" name="is_enabled" value="1" checked> Active
</label>
</div>
<div>
<div class="label" style="font-size:10px;">Releases in Bundle (Ctrl/Cmd-click for multi-select)</div>
<select class="input" name="release_ids[]" multiple size="8" required style="height:auto;">
<?php foreach ($bundleReleaseOptions as $opt): ?>
<option value="<?= (int)($opt['id'] ?? 0) ?>"><?= htmlspecialchars((string)($opt['label'] ?? ''), ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="display:flex; justify-content:flex-end;">
<button class="btn small" type="submit">Save Bundle</button>
</div>
</form>
</div>
<div class="admin-card" style="margin-top:12px; padding:14px;">
<div class="label" style="margin-bottom:10px;">Existing Bundles</div>
<?php if (!$bundles): ?>
<div style="color:var(--muted); font-size:13px;">No bundles yet.</div>
<?php else: ?>
<div style="overflow:auto;">
<table style="width:100%; border-collapse:separate; border-spacing:0 8px;">
<thead>
<tr style="color:var(--muted); font-size:10px; letter-spacing:.14em; text-transform:uppercase; text-align:left;">
<th style="padding:0 10px;">Bundle</th>
<th style="padding:0 10px;">Slug</th>
<th style="padding:0 10px;">Releases</th>
<th style="padding:0 10px;">Price</th>
<th style="padding:0 10px;">Status</th>
<th style="padding:0 10px; text-align:right;">Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($bundles as $b): ?>
<tr style="background:rgba(255,255,255,.02);">
<td style="padding:10px; border:1px solid rgba(255,255,255,.08); border-right:none; border-radius:10px 0 0 10px; font-weight:700;"><?= htmlspecialchars((string)($b['name'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
<td style="padding:10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08); color:var(--muted); font-size:12px;"><?= htmlspecialchars((string)($b['slug'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
<td style="padding:10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08); font-size:12px; color:var(--muted);"><?= (int)($b['release_count'] ?? 0) ?></td>
<td style="padding:10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08); font-size:12px;"><?= htmlspecialchars((string)($b['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($b['bundle_price'] ?? 0), 2) ?></td>
<td style="padding:10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08);"><span class="pill"><?= (int)($b['is_enabled'] ?? 0) === 1 ? 'active' : 'off' ?></span></td>
<td style="padding:10px; border:1px solid rgba(255,255,255,.08); border-left:none; border-radius:0 10px 10px 0; text-align:right;">
<form method="post" action="/admin/store/bundles/delete" onsubmit="return confirm('Delete this bundle?');" style="display:inline-flex;">
<input type="hidden" name="id" value="<?= (int)($b['id'] ?? 0) ?>">
<button class="btn outline small" type="submit" style="border-color:rgba(255,120,120,.45); color:#ffb9b9;">Delete</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<form method="post" action="/admin/store/settings" style="margin-top:16px; display:grid; gap:16px;">
<input type="hidden" name="tab" value="sales_chart">

View File

@@ -21,6 +21,9 @@ ob_start();
$key = (string)($item['key'] ?? '');
$title = (string)($item['title'] ?? 'Item');
$coverUrl = (string)($item['cover_url'] ?? '');
$itemType = (string)($item['item_type'] ?? 'track');
$releaseCount = (int)($item['release_count'] ?? 0);
$trackCount = (int)($item['track_count'] ?? 0);
$qty = max(1, (int)($item['qty'] ?? 1));
$price = (float)($item['price'] ?? 0);
$currency = (string)($item['currency'] ?? ($totals['currency'] ?? 'GBP'));
@@ -35,6 +38,18 @@ ob_start();
</div>
<div style="min-width:0;">
<div style="font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;"><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></div>
<?php if ($itemType === 'bundle' && ($releaseCount > 0 || $trackCount > 0)): ?>
<div style="font-size:12px; color:var(--muted); margin-top:4px;">
Includes
<?php if ($releaseCount > 0): ?>
<?= $releaseCount ?> release<?= $releaseCount === 1 ? '' : 's' ?>
<?php endif; ?>
<?php if ($releaseCount > 0 && $trackCount > 0): ?> · <?php endif; ?>
<?php if ($trackCount > 0): ?>
<?= $trackCount ?> track<?= $trackCount === 1 ? '' : 's' ?>
<?php endif; ?>
</div>
<?php endif; ?>
<div style="font-size:12px; color:var(--muted); margin-top:4px;"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price, 2) ?> x <?= $qty ?></div>
</div>
<div style="font-weight:700;"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price * $qty, 2) ?></div>

View File

@@ -1,208 +1,242 @@
<?php
declare(strict_types=1);
$pageTitle = $title ?? 'Checkout';
$items = is_array($items ?? null) ? $items : [];
$total = (float)($total ?? 0);
$subtotal = (float)($subtotal ?? $total);
$discountAmount = (float)($discount_amount ?? 0);
$discountCode = (string)($discount_code ?? '');
$currency = (string)($currency ?? 'GBP');
$success = (string)($success ?? '');
$orderNo = (string)($order_no ?? '');
$error = (string)($error ?? '');
$downloadLinks = is_array($download_links ?? null) ? $download_links : [];
$downloadNotice = (string)($download_notice ?? '');
$downloadLimit = (int)($download_limit ?? 5);
$downloadExpiryDays = (int)($download_expiry_days ?? 30);
ob_start();
?>
<section class="card checkout-wrap">
<div class="badge">Store</div>
<h1 style="margin:0; font-size:32px;">Checkout</h1>
<?php if ($success !== ''): ?>
<div style="padding:14px; border-radius:12px; border:1px solid rgba(34,242,165,.4); background:rgba(34,242,165,.12);">
<div style="font-weight:700;">Order complete</div>
<?php if ($orderNo !== ''): ?>
<div style="margin-top:4px; color:var(--muted);">Order: <?= htmlspecialchars($orderNo, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
</div>
<div class="checkout-panel">
<div class="badge" style="font-size:9px;">Your Downloads</div>
<?php if ($downloadLinks): ?>
<div style="display:grid; gap:10px; margin-top:10px;">
<?php foreach ($downloadLinks as $link): ?>
<?php
$label = trim((string)($link['label'] ?? 'Download'));
$url = trim((string)($link['url'] ?? ''));
if ($url === '') {
continue;
}
?>
<a href="<?= htmlspecialchars($url, ENT_QUOTES, 'UTF-8') ?>" class="checkout-download-link">
<span><?= htmlspecialchars($label !== '' ? $label : 'Download', ENT_QUOTES, 'UTF-8') ?></span>
<span class="checkout-download-link-arrow">Download</span>
</a>
<?php endforeach; ?>
</div>
<?php else: ?>
<p style="margin:10px 0 0; color:var(--muted); font-size:13px;">
<?= htmlspecialchars($downloadNotice !== '' ? $downloadNotice : 'No downloads available for this order yet.', ENT_QUOTES, 'UTF-8') ?>
</p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($error !== ''): ?>
<div style="padding:14px; border-radius:12px; border:1px solid rgba(243,176,176,.45); background:rgba(243,176,176,.12); color:#ffd6d6;">
<?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
</div>
<?php endif; ?>
<?php if (!$items): ?>
<div style="padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); background:rgba(0,0,0,.2); color:var(--muted);">
Your cart is empty.
</div>
<div><a href="/releases" class="btn">Browse releases</a></div>
<?php else: ?>
<div class="checkout-grid">
<div class="checkout-panel">
<div class="badge" style="font-size:9px;">Order Summary</div>
<div style="display:grid; gap:10px; margin-top:10px;">
<?php foreach ($items as $item): ?>
<?php
$title = (string)($item['title'] ?? 'Item');
$qty = max(1, (int)($item['qty'] ?? 1));
$price = (float)($item['price'] ?? 0);
$lineCurrency = (string)($item['currency'] ?? $currency);
?>
<div class="checkout-line">
<div class="checkout-line-title"><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></div>
<div class="checkout-line-meta"><?= htmlspecialchars($lineCurrency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price, 2) ?> x <?= $qty ?></div>
<div class="checkout-line-total"><?= htmlspecialchars($lineCurrency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price * $qty, 2) ?></div>
</div>
<?php endforeach; ?>
</div>
<div class="checkout-total">
<span>Subtotal</span>
<strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($subtotal, 2) ?></strong>
</div>
<?php if ($discountAmount > 0): ?>
<div class="checkout-total" style="margin-top:8px;">
<span>Discount (<?= htmlspecialchars($discountCode, ENT_QUOTES, 'UTF-8') ?>)</span>
<strong style="color:#9be7c6;">-<?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($discountAmount, 2) ?></strong>
</div>
<?php endif; ?>
<div class="checkout-total" style="margin-top:8px;">
<span>Order total</span>
<strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($total, 2) ?></strong>
</div>
</div>
<div class="checkout-panel">
<div class="badge" style="font-size:9px;">Buyer Details</div>
<form method="post" action="/checkout/place" style="display:grid; gap:12px; margin-top:10px;">
<label style="font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:.18em;">Email</label>
<input name="email" type="email" value="" placeholder="you@example.com" class="checkout-input" required>
<div class="checkout-terms">
<div class="badge" style="font-size:9px;">Terms</div>
<p style="margin:8px 0 0; color:var(--muted); font-size:13px; line-height:1.5;">
Digital download products are non-refundable once purchased and delivered. Please check your order details before placing the order.
Files are limited to <?= $downloadLimit ?> downloads and expire after <?= $downloadExpiryDays ?> days.
</p>
<label style="margin-top:10px; display:flex; gap:8px; align-items:flex-start; color:#d7def2; font-size:13px;">
<input type="checkbox" name="accept_terms" value="1" required style="margin-top:2px;">
<span>I agree to the terms and understand all sales are final.</span>
</label>
</div>
<button type="submit" class="checkout-place-btn">Place Order</button>
</form>
</div>
</div>
<?php endif; ?>
</section>
<style>
.checkout-wrap { display:grid; gap:14px; }
.checkout-grid { display:grid; grid-template-columns: minmax(0,1fr) 420px; gap:14px; }
.checkout-panel {
padding:14px;
border-radius:12px;
border:1px solid rgba(255,255,255,.1);
background:rgba(0,0,0,.2);
}
.checkout-line {
display:grid;
grid-template-columns:minmax(0,1fr) auto;
gap:8px;
padding:10px;
border-radius:10px;
border:1px solid rgba(255,255,255,.08);
background:rgba(255,255,255,.03);
}
.checkout-line-title { font-weight:600; }
.checkout-line-meta { color:var(--muted); font-size:12px; margin-top:4px; grid-column:1/2; }
.checkout-line-total { font-weight:700; grid-column:2/3; grid-row:1/3; align-self:center; }
.checkout-total {
margin-top:10px;
display:flex;
align-items:center;
justify-content:space-between;
padding:12px;
border-radius:10px;
border:1px solid rgba(255,255,255,.1);
background:rgba(255,255,255,.04);
}
.checkout-total strong { font-size:22px; }
.checkout-input {
height:40px;
border-radius:10px;
border:1px solid rgba(255,255,255,.2);
background:rgba(255,255,255,.05);
color:#fff;
padding:0 12px;
}
.checkout-terms {
padding:12px;
border-radius:10px;
border:1px solid rgba(255,255,255,.1);
background:rgba(255,255,255,.03);
}
.checkout-place-btn{
height:40px;
border-radius:999px;
border:1px solid rgba(34,242,165,.45);
background:rgba(34,242,165,.18);
color:#cbfff1;
font-weight:700;
letter-spacing:.1em;
text-transform:uppercase;
cursor:pointer;
}
.checkout-place-btn:hover { background:rgba(34,242,165,.28); }
.checkout-download-link {
display:flex;
align-items:center;
justify-content:space-between;
gap:10px;
padding:12px;
border-radius:10px;
border:1px solid rgba(34,242,165,.35);
background:rgba(34,242,165,.1);
color:#d7ffef;
text-decoration:none;
font-weight:600;
}
.checkout-download-link:hover { background:rgba(34,242,165,.18); }
.checkout-download-link-arrow {
font-size:11px;
text-transform:uppercase;
letter-spacing:.14em;
color:#8df7d1;
}
@media (max-width: 900px) {
.checkout-grid { grid-template-columns: 1fr; }
}
</style>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../../views/site/layout.php';
<?php
declare(strict_types=1);
$pageTitle = $title ?? 'Checkout';
$items = is_array($items ?? null) ? $items : [];
$total = (float)($total ?? 0);
$subtotal = (float)($subtotal ?? $total);
$discountAmount = (float)($discount_amount ?? 0);
$discountCode = (string)($discount_code ?? '');
$currency = (string)($currency ?? 'GBP');
$success = (string)($success ?? '');
$orderNo = (string)($order_no ?? '');
$error = (string)($error ?? '');
$downloadLinks = is_array($download_links ?? null) ? $download_links : [];
$downloadNotice = (string)($download_notice ?? '');
$downloadLimit = (int)($download_limit ?? 5);
$downloadExpiryDays = (int)($download_expiry_days ?? 30);
$paypalEnabled = (bool)($paypal_enabled ?? false);
$paypalCardsEnabled = (bool)($paypal_cards_enabled ?? false);
$paypalCardsAvailable = (bool)($paypal_cards_available ?? false);
ob_start();
?>
<section class="card checkout-wrap">
<div class="badge">Store</div>
<h1 style="margin:0; font-size:32px;">Checkout</h1>
<?php if ($success !== ''): ?>
<div class="checkout-status checkout-status-success">
<div style="font-weight:700;">Order complete</div>
<?php if ($orderNo !== ''): ?>
<div style="margin-top:4px; color:var(--muted);">Order: <?= htmlspecialchars($orderNo, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
</div>
<div class="checkout-panel">
<div class="badge" style="font-size:9px;">Your Downloads</div>
<?php if ($downloadLinks): ?>
<div style="display:grid; gap:10px; margin-top:10px;">
<?php foreach ($downloadLinks as $link): ?>
<?php
$label = trim((string)($link['label'] ?? 'Download'));
$url = trim((string)($link['url'] ?? ''));
if ($url === '') {
continue;
}
?>
<a href="<?= htmlspecialchars($url, ENT_QUOTES, 'UTF-8') ?>" class="checkout-download-link">
<span><?= htmlspecialchars($label !== '' ? $label : 'Download', ENT_QUOTES, 'UTF-8') ?></span>
<span class="checkout-download-link-arrow">Download</span>
</a>
<?php endforeach; ?>
</div>
<?php else: ?>
<p style="margin:10px 0 0; color:var(--muted); font-size:13px;">
<?= htmlspecialchars($downloadNotice !== '' ? $downloadNotice : 'No downloads available for this order yet.', ENT_QUOTES, 'UTF-8') ?>
</p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($error !== ''): ?>
<div class="checkout-status checkout-status-error"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<?php if (!$items): ?>
<div class="checkout-status checkout-status-empty">Your cart is empty.</div>
<div><a href="/releases" class="btn">Browse releases</a></div>
<?php else: ?>
<div class="checkout-grid">
<div class="checkout-panel">
<div class="badge" style="font-size:9px;">Order Summary</div>
<div style="display:grid; gap:10px; margin-top:10px;">
<?php foreach ($items as $item): ?>
<?php
$title = (string)($item['title'] ?? 'Item');
$itemType = (string)($item['item_type'] ?? 'track');
$releaseCount = (int)($item['release_count'] ?? 0);
$trackCount = (int)($item['track_count'] ?? 0);
$qty = max(1, (int)($item['qty'] ?? 1));
$price = (float)($item['price'] ?? 0);
$lineCurrency = (string)($item['currency'] ?? $currency);
?>
<div class="checkout-line">
<div class="checkout-line-title"><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></div>
<?php if ($itemType === 'bundle' && ($releaseCount > 0 || $trackCount > 0)): ?>
<div class="checkout-line-meta">
Includes
<?php if ($releaseCount > 0): ?><?= $releaseCount ?> release<?= $releaseCount === 1 ? '' : 's' ?><?php endif; ?>
<?php if ($releaseCount > 0 && $trackCount > 0): ?> &middot; <?php endif; ?>
<?php if ($trackCount > 0): ?><?= $trackCount ?> track<?= $trackCount === 1 ? '' : 's' ?><?php endif; ?>
</div>
<?php endif; ?>
<div class="checkout-line-meta"><?= htmlspecialchars($lineCurrency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price, 2) ?> x <?= $qty ?></div>
<div class="checkout-line-total"><?= htmlspecialchars($lineCurrency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price * $qty, 2) ?></div>
</div>
<?php endforeach; ?>
</div>
<div class="checkout-total">
<span>Subtotal</span>
<strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($subtotal, 2) ?></strong>
</div>
<?php if ($discountAmount > 0): ?>
<div class="checkout-total" style="margin-top:8px;">
<span>Discount (<?= htmlspecialchars($discountCode, ENT_QUOTES, 'UTF-8') ?>)</span>
<strong style="color:#9be7c6;">-<?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($discountAmount, 2) ?></strong>
</div>
<?php endif; ?>
<div class="checkout-total" style="margin-top:8px;">
<span>Order total</span>
<strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($total, 2) ?></strong>
</div>
</div>
<div class="checkout-panel">
<div class="badge" style="font-size:9px;">Buyer Details</div>
<form method="post" action="/checkout/place" class="checkout-form-stack" id="checkoutMethodForm">
<label class="checkout-label" for="checkoutEmail">Email</label>
<input id="checkoutEmail" name="email" type="email" value="" placeholder="you@example.com" class="checkout-input" required>
<div class="checkout-terms">
<div class="badge" style="font-size:9px;">Terms</div>
<p class="checkout-copy">
Digital download products are non-refundable once purchased and delivered. Please check your order details before placing the order.
Files are limited to <?= $downloadLimit ?> downloads and expire after <?= $downloadExpiryDays ?> days.
</p>
<label class="checkout-terms-check">
<input id="checkoutTerms" type="checkbox" name="accept_terms" value="1" required>
<span>I agree to the terms and understand all sales are final.</span>
</label>
</div>
<div class="checkout-payment-chooser">
<div class="checkout-payment-option">
<div class="badge" style="font-size:9px;">PayPal</div>
<div class="checkout-payment-title">Pay via PayPal</div>
<div class="checkout-copy">You will be redirected to PayPal to approve the payment.</div>
<button type="submit" class="checkout-place-btn"<?= $paypalEnabled ? '' : ' disabled' ?>>Pay via PayPal</button>
</div>
<?php if ($paypalCardsEnabled && $paypalCardsAvailable): ?>
<div class="checkout-payment-option">
<div class="badge" style="font-size:9px;">Cards</div>
<div class="checkout-payment-title">Pay via Credit / Debit Card</div>
<div class="checkout-copy">Open a dedicated secure card-payment page powered by PayPal.</div>
<button type="button" class="checkout-secondary-btn" id="checkoutCardStartBtn">Continue to card payment</button>
</div>
<?php endif; ?>
</div>
</form>
</div>
</div>
<?php endif; ?>
</section>
<style>
.checkout-wrap { display:grid; gap:14px; }
.checkout-grid { display:grid; grid-template-columns:minmax(0,1fr) 460px; gap:14px; }
.checkout-panel { padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); background:rgba(0,0,0,.2); }
.checkout-status { padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); }
.checkout-status-success { border-color:rgba(34,242,165,.4); background:rgba(34,242,165,.12); }
.checkout-status-error { border-color:rgba(243,176,176,.45); background:rgba(243,176,176,.12); color:#ffd6d6; }
.checkout-status-empty { background:rgba(0,0,0,.2); color:var(--muted); }
.checkout-form-stack { display:grid; gap:12px; margin-top:10px; }
.checkout-label { font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:.18em; }
.checkout-input { height:44px; border-radius:12px; border:1px solid rgba(255,255,255,.16); background:rgba(255,255,255,.05); color:#fff; padding:0 14px; }
.checkout-line { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; padding:10px; border-radius:10px; border:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.03); }
.checkout-line-title { font-weight:600; }
.checkout-line-meta { color:var(--muted); font-size:12px; margin-top:4px; grid-column:1/2; }
.checkout-line-total { font-weight:700; grid-column:2/3; grid-row:1/3; align-self:center; }
.checkout-total { margin-top:10px; display:flex; align-items:center; justify-content:space-between; padding:12px; border-radius:10px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.04); }
.checkout-total strong { font-size:22px; }
.checkout-terms { padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.03); }
.checkout-copy { margin:8px 0 0; color:var(--muted); font-size:13px; line-height:1.5; }
.checkout-terms-check { margin-top:10px; display:flex; gap:8px; align-items:flex-start; color:#d7def2; font-size:13px; }
.checkout-payment-chooser { display:grid; gap:12px; }
.checkout-payment-option { display:grid; gap:8px; padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.03); }
.checkout-payment-title { font-size:18px; font-weight:700; margin-top:4px; }
.checkout-place-btn, .checkout-secondary-btn { height:44px; border-radius:999px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; cursor:pointer; }
.checkout-place-btn { border:1px solid rgba(34,242,165,.45); background:rgba(34,242,165,.18); color:#cbfff1; }
.checkout-place-btn:hover { background:rgba(34,242,165,.28); }
.checkout-secondary-btn { border:1px solid rgba(255,255,255,.16); background:rgba(255,255,255,.05); color:#eef3ff; }
.checkout-secondary-btn:hover { background:rgba(255,255,255,.1); }
.checkout-download-link { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:12px; border-radius:10px; border:1px solid rgba(34,242,165,.35); background:rgba(34,242,165,.1); color:#d7ffef; text-decoration:none; font-weight:600; }
.checkout-download-link:hover { background:rgba(34,242,165,.18); }
.checkout-download-link-arrow { font-size:11px; text-transform:uppercase; letter-spacing:.14em; color:#8df7d1; }
@media (max-width: 900px) { .checkout-grid { grid-template-columns:1fr; } }
</style>
<script>
(function () {
var cardBtn = document.getElementById('checkoutCardStartBtn');
var form = document.getElementById('checkoutMethodForm');
var email = document.getElementById('checkoutEmail');
var terms = document.getElementById('checkoutTerms');
if (!cardBtn || !form || !email || !terms) {
return;
}
cardBtn.addEventListener('click', function () {
if (!email.reportValidity()) {
return;
}
if (!terms.checked) {
terms.reportValidity();
return;
}
var tmp = document.createElement('form');
tmp.method = 'post';
tmp.action = '/checkout/card/start';
tmp.style.display = 'none';
var emailInput = document.createElement('input');
emailInput.type = 'hidden';
emailInput.name = 'email';
emailInput.value = email.value;
tmp.appendChild(emailInput);
var termsInput = document.createElement('input');
termsInput.type = 'hidden';
termsInput.name = 'accept_terms';
termsInput.value = '1';
tmp.appendChild(termsInput);
var methodInput = document.createElement('input');
methodInput.type = 'hidden';
methodInput.name = 'checkout_method';
methodInput.value = 'card';
tmp.appendChild(methodInput);
var csrfMeta = document.querySelector('meta[name=\"csrf-token\"]');
if (csrfMeta && csrfMeta.content) {
var csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrf_token';
csrfInput.value = csrfMeta.content;
tmp.appendChild(csrfInput);
}
document.body.appendChild(tmp);
tmp.submit();
});
})();
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../../views/site/layout.php';

View File

@@ -0,0 +1,310 @@
<?php
declare(strict_types=1);
$pageTitle = $title ?? 'Card Checkout';
$items = is_array($items ?? null) ? $items : [];
$total = (float)($total ?? 0);
$subtotal = (float)($subtotal ?? $total);
$discountAmount = (float)($discount_amount ?? 0);
$discountCode = (string)($discount_code ?? '');
$currency = (string)($currency ?? 'GBP');
$email = (string)($email ?? '');
$acceptTerms = (bool)($accept_terms ?? false);
$downloadLimit = (int)($download_limit ?? 5);
$downloadExpiryDays = (int)($download_expiry_days ?? 30);
$paypalClientId = trim((string)($paypal_client_id ?? ''));
$paypalClientToken = trim((string)($paypal_client_token ?? ''));
$paypalMerchantCountry = strtoupper(trim((string)($paypal_merchant_country ?? '')));
$paypalCardBrandingText = trim((string)($paypal_card_branding_text ?? 'Pay with card'));
$sdkUrl = 'https://www.paypal.com/sdk/js?client-id=' . rawurlencode($paypalClientId)
. '&currency=' . rawurlencode($currency)
. '&intent=capture'
. '&components=buttons,card-fields';
ob_start();
?>
<section class="card checkout-wrap">
<div class="badge">Cards</div>
<div class="checkout-card-header">
<div>
<h1 style="margin:0; font-size:32px;">Credit / Debit Card</h1>
<p class="checkout-card-copy">Secure card payment powered by PayPal. The order completes immediately after capture succeeds.</p>
</div>
<a href="/checkout" class="btn outline">Back to checkout</a>
</div>
<div class="checkout-grid">
<div class="checkout-panel">
<div class="badge" style="font-size:9px;">Buyer Details</div>
<div class="checkout-form-stack">
<label class="checkout-label" for="cardCheckoutEmail">Email</label>
<input id="cardCheckoutEmail" type="email" value="<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>" placeholder="you@example.com" class="checkout-input" required>
<div class="checkout-terms">
<div class="badge" style="font-size:9px;">Terms</div>
<p class="checkout-card-copy">
Digital download products are non-refundable once purchased and delivered. Please check your order details before placing the order.
Files are limited to <?= $downloadLimit ?> downloads and expire after <?= $downloadExpiryDays ?> days.
</p>
<label class="checkout-terms-check">
<input id="cardCheckoutTerms" type="checkbox" value="1" <?= $acceptTerms ? 'checked' : '' ?>>
<span>I agree to the terms and understand all sales are final.</span>
</label>
</div>
<div id="cardCheckoutStatus" class="checkout-inline-status" hidden></div>
<div class="checkout-card-form">
<div class="checkout-card-field">
<label class="checkout-label">Cardholder name</label>
<div id="paypal-name-field" class="checkout-card-shell"></div>
</div>
<div class="checkout-card-field checkout-card-field-full">
<label class="checkout-label">Card number</label>
<div id="paypal-number-field" class="checkout-card-shell"></div>
</div>
<div class="checkout-card-field">
<label class="checkout-label">Expiry</label>
<div id="paypal-expiry-field" class="checkout-card-shell"></div>
</div>
<div class="checkout-card-field">
<label class="checkout-label">Security code</label>
<div id="paypal-cvv-field" class="checkout-card-shell"></div>
</div>
</div>
<button type="button" id="paypalCardSubmit" class="checkout-place-btn"><?= htmlspecialchars($paypalCardBrandingText !== '' ? $paypalCardBrandingText : 'Pay with card', ENT_QUOTES, 'UTF-8') ?></button>
</div>
</div>
<div class="checkout-panel">
<div class="badge" style="font-size:9px;">Order Summary</div>
<div style="display:grid; gap:10px; margin-top:10px;">
<?php foreach ($items as $item): ?>
<?php
$title = (string)($item['title'] ?? 'Item');
$qty = max(1, (int)($item['qty'] ?? 1));
$price = (float)($item['price'] ?? 0);
$lineCurrency = (string)($item['currency'] ?? $currency);
?>
<div class="checkout-line">
<div class="checkout-line-title"><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></div>
<div class="checkout-line-meta"><?= htmlspecialchars($lineCurrency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price, 2) ?> x <?= $qty ?></div>
<div class="checkout-line-total"><?= htmlspecialchars($lineCurrency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price * $qty, 2) ?></div>
</div>
<?php endforeach; ?>
</div>
<div class="checkout-total">
<span>Subtotal</span>
<strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($subtotal, 2) ?></strong>
</div>
<?php if ($discountAmount > 0): ?>
<div class="checkout-total" style="margin-top:8px;">
<span>Discount (<?= htmlspecialchars($discountCode, ENT_QUOTES, 'UTF-8') ?>)</span>
<strong style="color:#9be7c6;">-<?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($discountAmount, 2) ?></strong>
</div>
<?php endif; ?>
<div class="checkout-total" style="margin-top:8px;">
<span>Order total</span>
<strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($total, 2) ?></strong>
</div>
</div>
</div>
</section>
<style>
.checkout-wrap { display:grid; gap:14px; }
.checkout-card-header { display:flex; align-items:start; justify-content:space-between; gap:16px; }
.checkout-grid { display:grid; grid-template-columns:minmax(0, 1.1fr) 420px; gap:14px; }
.checkout-panel { padding:16px; border-radius:14px; border:1px solid rgba(255,255,255,.1); background:rgba(0,0,0,.2); }
.checkout-form-stack { display:grid; gap:12px; margin-top:10px; }
.checkout-label { font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:.18em; }
.checkout-input { height:46px; border-radius:12px; border:1px solid rgba(255,255,255,.16); background:rgba(255,255,255,.05); color:#fff; padding:0 14px; }
.checkout-terms { padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.03); }
.checkout-card-copy { margin:8px 0 0; color:var(--muted); font-size:13px; line-height:1.55; }
.checkout-terms-check { margin-top:10px; display:flex; gap:8px; align-items:flex-start; color:#d7def2; font-size:13px; }
.checkout-inline-status { padding:12px 14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); font-size:13px; }
.checkout-inline-status.error { border-color:rgba(243,176,176,.45); background:rgba(243,176,176,.12); color:#ffd6d6; }
.checkout-inline-status.info { border-color:rgba(255,255,255,.12); background:rgba(255,255,255,.04); color:#d7def2; }
.checkout-card-form { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; padding:12px; border-radius:16px; border:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.02); }
.checkout-card-field-full { grid-column:1 / -1; }
.checkout-card-field { display:grid; gap:6px; }
.checkout-card-shell { min-height:44px; border-radius:12px; border:0; background:transparent; box-shadow:none; padding:0; display:flex; align-items:center; }
.checkout-card-shell iframe { width:100% !important; min-height:40px !important; border-radius:10px !important; }
.checkout-place-btn { height:48px; border-radius:999px; border:1px solid rgba(34,242,165,.45); background:rgba(34,242,165,.18); color:#cbfff1; font-weight:700; letter-spacing:.1em; text-transform:uppercase; cursor:pointer; margin-top:4px; }
.checkout-place-btn:hover { background:rgba(34,242,165,.28); }
.checkout-line { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; padding:10px; border-radius:10px; border:1px solid rgba(255,255,255,.08); background:rgba(255,255,255,.03); }
.checkout-line-title { font-weight:600; }
.checkout-line-meta { color:var(--muted); font-size:12px; margin-top:4px; grid-column:1/2; }
.checkout-line-total { font-weight:700; grid-column:2/3; grid-row:1/3; align-self:center; }
.checkout-total { margin-top:10px; display:flex; align-items:center; justify-content:space-between; padding:12px; border-radius:10px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.04); }
.checkout-total strong { font-size:22px; }
@media (max-width: 900px) {
.checkout-card-header { flex-direction:column; align-items:stretch; }
.checkout-grid { grid-template-columns:1fr; }
.checkout-card-form { grid-template-columns:1fr; }
.checkout-card-field-full { grid-column:auto; }
}
</style>
<script src="<?= htmlspecialchars($sdkUrl, ENT_QUOTES, 'UTF-8') ?>" data-client-token="<?= htmlspecialchars($paypalClientToken, ENT_QUOTES, 'UTF-8') ?>" data-sdk-integration-source="audiocore"></script>
<script>
(function () {
var emailEl = document.getElementById('cardCheckoutEmail');
var termsEl = document.getElementById('cardCheckoutTerms');
var statusEl = document.getElementById('cardCheckoutStatus');
var submitBtn = document.getElementById('paypalCardSubmit');
function setStatus(type, message) {
if (!statusEl) return;
if (!message) {
statusEl.hidden = true;
statusEl.textContent = '';
statusEl.className = 'checkout-inline-status';
return;
}
statusEl.hidden = false;
statusEl.textContent = message;
statusEl.className = 'checkout-inline-status ' + type;
}
function validateBuyer() {
var email = emailEl ? emailEl.value.trim() : '';
if (!email) {
setStatus('error', 'Enter your email address.');
return null;
}
if (!termsEl || !termsEl.checked) {
setStatus('error', 'Accept the terms to continue.');
return null;
}
setStatus('', '');
return { email: email, accept_terms: true };
}
function postJson(url, payload) {
var csrfMeta = document.querySelector('meta[name=\"csrf-token\"]');
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-Token': csrfMeta ? csrfMeta.content : '' },
body: JSON.stringify(payload)
}).then(function (response) {
return response.json().catch(function () {
return { ok: false, error: 'Unexpected server response.' };
});
});
}
function createOrder() {
var buyer = validateBuyer();
if (!buyer) {
return Promise.reject(new Error('Validation failed.'));
}
setStatus('info', 'Preparing secure card payment...');
return postJson('/checkout/paypal/create-order', buyer).then(function (data) {
if (!data || data.ok !== true) {
throw new Error((data && data.error) || 'Unable to start checkout.');
}
if (data.completed && data.redirect) {
window.location.href = data.redirect;
throw new Error('redirect');
}
return data.orderID || data.paypal_order_id;
});
}
function captureOrder(orderID) {
setStatus('info', 'Finalizing payment...');
return postJson('/checkout/paypal/capture-order', { orderID: orderID }).then(function (data) {
if (!data || data.ok !== true) {
throw new Error((data && data.error) || 'Unable to finalize payment.');
}
if (data.redirect) {
window.location.href = data.redirect;
}
});
}
function initCardFields() {
if (!(window.paypal && paypal.CardFields)) {
setStatus('error', 'PayPal card fields failed to load. Client token present: <?= $paypalClientToken !== '' ? 'yes' : 'no' ?>. Capability requires Advanced Card Payments on the PayPal account.');
if (submitBtn) submitBtn.disabled = true;
return;
}
var cardFields = paypal.CardFields({
style: {
'input': {
'appearance': 'none',
'background': '#151922',
'color': '#eef3ff',
'font-family': 'Syne, sans-serif',
'font-size': '17px',
'line-height': '24px',
'padding': '12px 14px',
'border': '1px solid rgba(255,255,255,0.10)',
'border-radius': '10px',
'box-shadow': 'none',
'outline': 'none',
'-webkit-appearance': 'none'
},
'input::placeholder': {
'color': 'rgba(238,243,255,0.42)'
},
'input:hover': {
'border': '1px solid rgba(255,255,255,0.18)'
},
'input:focus': {
'border': '1px solid #22f2a5',
'box-shadow': '0 0 0 2px rgba(34,242,165,0.12)'
},
'.valid': {
'color': '#eef3ff'
},
'.invalid': {
'color': '#ffd6d6',
'border': '1px solid rgba(255,107,107,0.72)',
'box-shadow': '0 0 0 2px rgba(255,107,107,0.10)'
}
},
createOrder: function () {
return createOrder();
},
onApprove: function (data) {
return captureOrder(data.orderID);
},
onError: function (err) {
setStatus('error', err && err.message ? err.message : 'Card payment failed.');
}
});
if (!cardFields.isEligible()) {
setStatus('error', 'Card checkout is not available for this account.');
if (submitBtn) submitBtn.disabled = true;
return;
}
cardFields.NameField().render('#paypal-name-field');
cardFields.NumberField().render('#paypal-number-field');
cardFields.ExpiryField().render('#paypal-expiry-field');
cardFields.CVVField().render('#paypal-cvv-field');
if (submitBtn) {
submitBtn.addEventListener('click', function () {
if (!validateBuyer()) {
return;
}
setStatus('info', 'Submitting card payment...');
cardFields.submit({});
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCardFields);
} else {
initCardFields();
}
}());
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/../../../../views/site/layout.php';