v1.0.0.1
StocketBase
- <?php
-
- declare(strict_types=1);
-
- require_once __DIR__ . '/includes/config.php';
- require_once __DIR__ . '/includes/database.php';
- require_once __DIR__ . '/includes/user-auth.php';
- require_once __DIR__ . '/includes/csrf.php';
- require_once __DIR__ . '/includes/language.php';
- require_once __DIR__ . '/includes/site-front.php';
- require_once __DIR__ . '/includes/cart.php';
- require_once __DIR__ . '/includes/currency.php';
-
- if (is_file(__DIR__ . '/includes/payments/stripe-client.php')) {
- require_once __DIR__ . '/includes/payments/stripe-client.php';
- }
- if (is_file(__DIR__ . '/includes/payments/polar-client.php')) {
- require_once __DIR__ . '/includes/payments/polar-client.php';
- }
-
- UserAuth::boot();
- Cart::boot();
-
- $db = Database::site();
- $usersDb = Database::users();
- $settings = SiteFront::settings();
-
- function checkoutBaseUrl(): string
- {
- return rtrim((string) Config::get('APP_URL', ''), '/') . Config::get('APP_BASE_PATH', '');
- }
-
- function checkoutGenerateOrderNumber(PDO $usersDb): string
- {
- do {
- $candidate = 'ORD-' . date('Ymd') . '-' . strtoupper(bin2hex(random_bytes(3)));
- $statement = $usersDb->prepare('SELECT id FROM orders WHERE order_number = :n LIMIT 1');
- $statement->execute(['n' => $candidate]);
- } while ($statement->fetch());
-
- return $candidate;
- }
-
- $isSubmit = $_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'submit_order';
- $requestProductId = (int) ($_REQUEST['product_id'] ?? 0);
- $mode = $requestProductId > 0 ? 'single' : 'cart';
- $provider = (string) ($_REQUEST['provider'] ?? $settings['payment_provider']);
- if (!in_array($provider, ['stripe', 'polar'], true)) {
- $provider = 'stripe';
- }
- if ($provider === 'polar' && ($settings['store_category'] ?? 'general') !== 'saas') {
- $provider = 'stripe';
- }
-
- $errorMessage = null;
- $singleProduct = null;
-
- if ($isSubmit && !Csrf::verify($_POST['csrf_token'] ?? null)) {
- $isSubmit = false;
- $errorMessage = Language::get('form_security_failed', 'Security check failed, please try again.');
- }
-
- if ($mode === 'single') {
- $statement = $db->prepare("SELECT * FROM products WHERE id = :id AND status = 'published' LIMIT 1");
- $statement->execute(['id' => $requestProductId]);
- $singleProduct = $statement->fetch();
-
- if (!$singleProduct) {
- http_response_code(404);
- $pageTitle = Language::get('page_title_not_found', 'Not found') . ' - ' . $settings['site_name'];
- require __DIR__ . '/includes/front-header.php';
- echo '<p>' . htmlspecialchars(Language::get('product_not_found', 'Product not found.')) . '</p>';
- require __DIR__ . '/includes/front-footer.php';
- exit;
- }
-
- $singleIsAllowed = ($settings['pricing_model'] ?? 'per_product') === 'subscription' || $provider === 'polar';
- if (!$singleIsAllowed) {
- header('Location: ' . SiteFront::productUrl((string) $singleProduct['slug']));
- exit;
- }
- } elseif (Cart::isEmpty()) {
- header('Location: cart.php');
- exit;
- }
-
- $loggedInCustomer = UserAuth::user();
-
- if ($loggedInCustomer === null && ($mode === 'single' || ($settings['require_account_to_purchase'] ?? '0') === '1')) {
- $checkoutReturnTo = 'checkout.php';
- if ($mode === 'single') {
- $checkoutReturnTo .= '?product_id=' . $requestProductId . ($provider === 'polar' ? '&provider=polar' : '');
- }
- header('Location: user-login.php?redirect=' . urlencode($checkoutReturnTo));
- exit;
- }
-
- $checkoutRequiresShipping = $mode === 'cart' && Cart::requiresShipping();
-
- if ($isSubmit && $mode === 'single') {
- $customerEmail = $loggedInCustomer['email'] ?? trim((string) ($_POST['email'] ?? ''));
- if ($customerEmail === '' || !filter_var($customerEmail, FILTER_VALIDATE_EMAIL)) {
- $errorMessage = Language::get('checkout_error_email', 'Please provide a valid email address.');
- } else {
- $productCurrency = (string) ($singleProduct['currency'] ?: $settings['store_currency']);
- $billingInterval = (string) ($singleProduct['billing_interval'] ?: 'month');
- $successUrl = checkoutBaseUrl() . '/order-confirmation.php?session_id={CHECKOUT_SESSION_ID}';
- $cancelUrl = checkoutBaseUrl() . SiteFront::productUrl((string) $singleProduct['slug']);
- $metadata = ['product_id' => (string) $singleProduct['id'], 'checkout_kind' => 'subscription'];
- if ($loggedInCustomer !== null) {
- $metadata['user_account_id'] = (string) $loggedInCustomer['id'];
- }
-
- if ($provider === 'polar') {
- if (!class_exists('PolarClient')) {
- $errorMessage = Language::get('checkout_error_polar_config', 'Polar checkout is not configured yet. Please try again later.');
- } else {
- $polarProductId = (string) ($singleProduct['polar_product_id'] ?? '');
- if ($polarProductId === '') {
- $errorMessage = Language::get('checkout_error_polar_product', 'This product is not linked to a Polar plan yet.');
- } else {
- $session = (new PolarClient())->createCheckoutSession($polarProductId, $successUrl, $customerEmail, $metadata);
- if (is_array($session) && !empty($session['url'])) {
- header('Location: ' . $session['url']);
- exit;
- }
- $errorMessage = Language::get('checkout_error_polar_start', 'Could not start Polar checkout. Please try again.');
- }
- }
- } else {
- if (!class_exists('StripeClient')) {
- $errorMessage = Language::get('checkout_error_payment_config', 'Payment is not configured yet. Please try again later.');
- } elseif (empty($singleProduct['price_cents'])) {
- $errorMessage = Language::get('checkout_error_no_price', 'This product does not have a price configured.');
- } else {
- $isOneTime = $billingInterval === 'one_time';
- $lineItems = [[
- 'name' => (string) $singleProduct['title'],
- 'unit_amount_cents' => (int) $singleProduct['price_cents'],
- 'currency' => strtolower($productCurrency),
- 'quantity' => 1,
- 'recurring_interval' => $isOneTime ? null : ($billingInterval === 'year' ? 'year' : 'month'),
- ]];
- if ($isOneTime) {
- $metadata['checkout_kind'] = 'one_time';
- }
-
- $session = (new StripeClient())->createCheckoutSession($lineItems, $isOneTime ? 'payment' : 'subscription', $successUrl, $cancelUrl, $customerEmail, $metadata);
- if (is_array($session) && !empty($session['url'])) {
- header('Location: ' . $session['url']);
- exit;
- }
- $errorMessage = Language::get('checkout_error_stripe_start', 'Could not start the payment. Please try again.');
- }
- }
- }
- }
-
- $formValues = [
- 'email' => $loggedInCustomer['email'] ?? '',
- 'shipping_name' => '',
- 'shipping_address_line1' => '',
- 'shipping_address_line2' => '',
- 'shipping_city' => '',
- 'shipping_region' => '',
- 'shipping_postal_code' => '',
- 'shipping_country' => '',
- 'billing_same_as_shipping' => '1',
- 'billing_name' => '',
- 'billing_address_line1' => '',
- 'billing_address_line2' => '',
- 'billing_city' => '',
- 'billing_region' => '',
- 'billing_postal_code' => '',
- 'billing_country' => '',
- 'customer_note' => '',
- ];
-
- if ($isSubmit && $mode === 'cart') {
- foreach ($formValues as $field => $default) {
- $formValues[$field] = trim((string) ($_POST[$field] ?? $default));
- }
- $sameAsShipping = ($_POST['billing_same_as_shipping'] ?? '') === '1';
-
- $items = Cart::items();
- if (empty($items)) {
- header('Location: cart.php');
- exit;
- }
-
- $customerEmail = $loggedInCustomer['email'] ?? $formValues['email'];
- $itemCurrencies = array_unique(array_map(static fn(array $item): string => strtoupper((string) $item['currency']), $items));
-
- if (count($itemCurrencies) > 1) {
- $errorMessage = Language::get('checkout_error_currencies', 'Your cart contains products priced in different currencies. Please check out products in one currency at a time.');
- } elseif ($customerEmail === '' || !filter_var($customerEmail, FILTER_VALIDATE_EMAIL)) {
- $errorMessage = Language::get('checkout_error_email', 'Please provide a valid email address.');
- } elseif ($checkoutRequiresShipping && ($formValues['shipping_name'] === '' || $formValues['shipping_address_line1'] === '' || $formValues['shipping_city'] === '' || $formValues['shipping_postal_code'] === '' || $formValues['shipping_country'] === '')) {
- $errorMessage = Language::get('checkout_error_shipping', 'Please fill in all required shipping fields.');
- } else {
- $cartCurrency = Cart::currency();
- $subtotalCents = Cart::subtotalCents();
-
- $appliedCode = Cart::appliedDiscountCode();
- $discount = $appliedCode !== null ? Cart::findDiscount($appliedCode, $cartCurrency) : null;
- $discountCents = $discount !== null ? Cart::discountAmountCents($discount, $subtotalCents) : 0;
-
- $shippingCents = 0;
- $taxCents = 0;
- $totalCents = max(0, $subtotalCents - $discountCents + $shippingCents + $taxCents);
-
- $billingName = $sameAsShipping ? $formValues['shipping_name'] : $formValues['billing_name'];
- $billingLine1 = $sameAsShipping ? $formValues['shipping_address_line1'] : $formValues['billing_address_line1'];
- $billingLine2 = $sameAsShipping ? $formValues['shipping_address_line2'] : $formValues['billing_address_line2'];
- $billingCity = $sameAsShipping ? $formValues['shipping_city'] : $formValues['billing_city'];
- $billingRegion = $sameAsShipping ? $formValues['shipping_region'] : $formValues['billing_region'];
- $billingPostal = $sameAsShipping ? $formValues['shipping_postal_code'] : $formValues['billing_postal_code'];
- $billingCountry = $sameAsShipping ? $formValues['shipping_country'] : $formValues['billing_country'];
-
- $orderNumber = checkoutGenerateOrderNumber($usersDb);
-
- try {
- $usersDb->beginTransaction();
-
- $insertOrder = $usersDb->prepare(
- "INSERT INTO orders (
- order_number, user_account_id, guest_email, status, provider, currency,
- subtotal_cents, discount_cents, shipping_cents, tax_cents, total_cents, discount_code,
- shipping_name, shipping_address_line1, shipping_address_line2, shipping_city, shipping_region, shipping_postal_code, shipping_country,
- billing_same_as_shipping, billing_name, billing_address_line1, billing_address_line2, billing_city, billing_region, billing_postal_code, billing_country,
- customer_note
- ) VALUES (
- :order_number, :user_account_id, :guest_email, 'pending', 'stripe', :currency,
- :subtotal_cents, :discount_cents, :shipping_cents, :tax_cents, :total_cents, :discount_code,
- :shipping_name, :shipping_address_line1, :shipping_address_line2, :shipping_city, :shipping_region, :shipping_postal_code, :shipping_country,
- :billing_same_as_shipping, :billing_name, :billing_address_line1, :billing_address_line2, :billing_city, :billing_region, :billing_postal_code, :billing_country,
- :customer_note
- )"
- );
- $insertOrder->execute([
- 'order_number' => $orderNumber,
- 'user_account_id' => $loggedInCustomer['id'] ?? null,
- 'guest_email' => $loggedInCustomer === null ? $customerEmail : null,
- 'currency' => $cartCurrency,
- 'subtotal_cents' => $subtotalCents,
- 'discount_cents' => $discountCents,
- 'shipping_cents' => $shippingCents,
- 'tax_cents' => $taxCents,
- 'total_cents' => $totalCents,
- 'discount_code' => $appliedCode,
- 'shipping_name' => $formValues['shipping_name'] ?: null,
- 'shipping_address_line1' => $formValues['shipping_address_line1'] ?: null,
- 'shipping_address_line2' => $formValues['shipping_address_line2'] ?: null,
- 'shipping_city' => $formValues['shipping_city'] ?: null,
- 'shipping_region' => $formValues['shipping_region'] ?: null,
- 'shipping_postal_code' => $formValues['shipping_postal_code'] ?: null,
- 'shipping_country' => $formValues['shipping_country'] !== '' ? strtoupper(substr($formValues['shipping_country'], 0, 2)) : null,
- 'billing_same_as_shipping' => $sameAsShipping ? 1 : 0,
- 'billing_name' => $billingName ?: null,
- 'billing_address_line1' => $billingLine1 ?: null,
- 'billing_address_line2' => $billingLine2 ?: null,
- 'billing_city' => $billingCity ?: null,
- 'billing_region' => $billingRegion ?: null,
- 'billing_postal_code' => $billingPostal ?: null,
- 'billing_country' => $billingCountry !== '' ? strtoupper(substr($billingCountry, 0, 2)) : null,
- 'customer_note' => $formValues['customer_note'] !== '' ? mb_substr($formValues['customer_note'], 0, 2000) : null,
- ]);
- $orderId = (int) $usersDb->lastInsertId();
-
- $insertItem = $usersDb->prepare(
- "INSERT INTO order_items (order_id, product_id, variant_id, product_title_snapshot, variant_name_snapshot, unit_price_cents, quantity, total_cents)
- VALUES (:order_id, :product_id, :variant_id, :title, :variant_name, :unit_price_cents, :quantity, :total_cents)"
- );
- foreach ($items as $item) {
- $insertItem->execute([
- 'order_id' => $orderId,
- 'product_id' => $item['product_id'],
- 'variant_id' => $item['variant_id'],
- 'title' => $item['title'],
- 'variant_name' => $item['variant_name'],
- 'unit_price_cents' => $item['unit_price_cents'],
- 'quantity' => $item['quantity'],
- 'total_cents' => $item['total_cents'],
- ]);
- }
-
- $insertHistory = $usersDb->prepare(
- "INSERT INTO order_status_history (order_id, status, note) VALUES (:order_id, 'pending', 'Order created, awaiting payment')"
- );
- $insertHistory->execute(['order_id' => $orderId]);
-
- $usersDb->commit();
- } catch (Throwable $exception) {
- if ($usersDb->inTransaction()) {
- $usersDb->rollBack();
- }
- error_log('StocketBase: could not create order: ' . $exception->getMessage());
- $orderId = null;
- }
-
- if (!isset($orderId) || $orderId === null) {
- $errorMessage = Language::get('checkout_error_order', 'Could not create your order. Please try again.');
- } else {
- if (!isset($_SESSION['checkout_order_ids']) || !is_array($_SESSION['checkout_order_ids'])) {
- $_SESSION['checkout_order_ids'] = [];
- }
- $_SESSION['checkout_order_ids'][] = $orderId;
- $_SESSION['checkout_order_ids'] = array_slice(array_unique($_SESSION['checkout_order_ids']), -20);
-
- $lineItems = [];
- foreach ($items as $item) {
- $name = $item['title'] . ($item['variant_name'] !== null ? ' — ' . $item['variant_name'] : '');
- $lineItems[] = [
- 'name' => $name,
- 'unit_amount_cents' => (int) $item['unit_price_cents'],
- 'currency' => strtolower($item['currency']),
- 'quantity' => (int) $item['quantity'],
- ];
- }
-
- $successUrl = checkoutBaseUrl() . '/order-confirmation.php?order_id=' . $orderId . '&session_id={CHECKOUT_SESSION_ID}';
- $cancelUrl = checkoutBaseUrl() . '/cart.php';
- $metadata = ['order_id' => (string) $orderId, 'checkout_kind' => 'order'];
-
- if (!class_exists('StripeClient')) {
- $errorMessage = Language::get('checkout_error_payment_config', 'Payment is not configured yet. Please try again later.');
- } else {
- $stripe = new StripeClient();
- $couponId = null;
- if ($discountCents > 0) {
- $couponId = $stripe->createOneTimeCoupon($discountCents, $cartCurrency, Language::get('checkout_discount_name', 'Discount') . ' ' . (string) $appliedCode);
- }
-
- if ($discountCents > 0 && $couponId === null) {
- $errorMessage = Language::get('checkout_error_discount', 'Could not apply your discount at payment. Please try again.');
- } else {
- $session = $stripe->createCheckoutSession($lineItems, 'payment', $successUrl, $cancelUrl, $customerEmail, $metadata, $couponId);
- if (is_array($session) && !empty($session['url'])) {
- $usersDb->prepare('UPDATE orders SET provider_checkout_id = :checkout_id WHERE id = :id')
- ->execute(['checkout_id' => (string) ($session['id'] ?? ''), 'id' => $orderId]);
- header('Location: ' . $session['url']);
- exit;
- }
- $errorMessage = Language::get('checkout_error_stripe_start', 'Could not start the payment. Please try again.');
- }
- }
-
- if ($errorMessage !== null) {
- $usersDb->prepare("UPDATE orders SET status = 'failed' WHERE id = :id AND status = 'pending'")
- ->execute(['id' => $orderId]);
- }
- }
- }
- }
-
- $pageTitle = Language::get('checkout_title', 'Checkout') . ' - ' . $settings['site_name'];
- $pageDescription = '';
-
- require __DIR__ . '/includes/front-header.php';
-
- ?>
- <h1 class="article-title"><?= htmlspecialchars(Language::get('checkout_title', 'Checkout')) ?></h1>
-
- <?php foreach (SiteFront::activeAds('checkout') as $checkoutAd): ?>
- <?= SiteFront::renderAd($checkoutAd) ?>
- <?php endforeach; ?>
-
- <?php if ($errorMessage !== null): ?>
- <p style="color: var(--danger);"><?= htmlspecialchars($errorMessage) ?></p>
- <?php endif; ?>
-
- <?php if ($mode === 'single'): ?>
- <?php
- $productCurrency = (string) ($singleProduct['currency'] ?: $settings['store_currency']);
- $billingInterval = (string) ($singleProduct['billing_interval'] ?: 'month');
- ?>
- <div class="checkout-summary">
- <h2><?= htmlspecialchars($singleProduct['title']) ?></h2>
- <p>
- <?= htmlspecialchars(Currency::format((int) $singleProduct['price_cents'], $productCurrency)) ?>
- / <?= htmlspecialchars($billingInterval === 'year' ? Language::get('subscription_per_year', 'year') : Language::get('subscription_per_month', 'month')) ?>
- <?= $provider === 'polar' ? ' · Polar' : ' · Stripe' ?>
- </p>
- </div>
-
- <form method="post" class="checkout-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="submit_order">
- <input type="hidden" name="product_id" value="<?= (int) $singleProduct['id'] ?>">
- <input type="hidden" name="provider" value="<?= htmlspecialchars($provider, ENT_QUOTES) ?>">
-
- <?php if ($loggedInCustomer === null): ?>
- <label for="email"><?= htmlspecialchars(Language::get('checkout_email_label', 'Email address')) ?></label>
- <input type="email" name="email" id="email" required value="<?= htmlspecialchars($formValues['email']) ?>">
- <?php endif; ?>
-
- <button type="submit" class="unlock-btn" style="border:none;cursor:pointer;margin-top:16px;">
- <?= $provider === 'polar' ? htmlspecialchars(Language::get('product_subscribe_polar_button', 'Subscribe via Polar')) : htmlspecialchars(Language::get('product_subscribe_button', 'Subscribe')) ?>
- </button>
- </form>
- <?php else: ?>
- <?php
- $items = Cart::items();
- $cartCurrency = Cart::currency();
- $subtotalCents = Cart::subtotalCents();
- $appliedCode = Cart::appliedDiscountCode();
- $discount = $appliedCode !== null ? Cart::findDiscount($appliedCode, $cartCurrency) : null;
- $discountCents = $discount !== null ? Cart::discountAmountCents($discount, $subtotalCents) : 0;
- $totalCents = max(0, $subtotalCents - $discountCents);
- ?>
- <div class="checkout-summary">
- <ul>
- <?php foreach ($items as $item): ?>
- <li><?= htmlspecialchars($item['title']) ?><?= $item['variant_name'] !== null ? ' — ' . htmlspecialchars($item['variant_name']) : '' ?> × <?= (int) $item['quantity'] ?> — <?= htmlspecialchars(Currency::format($item['total_cents'], $item['currency'])) ?></li>
- <?php endforeach; ?>
- </ul>
- <div class="cart-summary-row"><span><?= htmlspecialchars(Language::get('cart_subtotal', 'Subtotal')) ?></span><span><?= htmlspecialchars(Currency::format($subtotalCents, $cartCurrency)) ?></span></div>
- <?php if ($discountCents > 0): ?>
- <div class="cart-summary-row"><span><?= htmlspecialchars(Language::get('cart_discount', 'Discount')) ?></span><span>-<?= htmlspecialchars(Currency::format($discountCents, $cartCurrency)) ?></span></div>
- <?php endif; ?>
- <div class="cart-summary-row cart-summary-total"><span><?= htmlspecialchars(Language::get('cart_total', 'Total')) ?></span><span><?= htmlspecialchars(Currency::format($totalCents, $cartCurrency)) ?></span></div>
- <p class="cart-tax-note"><?= htmlspecialchars(Language::get('checkout_tax_note', 'Tax is not calculated in this version.')) ?></p>
- </div>
-
- <form method="post" class="checkout-form">
- <?= Csrf::field() ?>
- <input type="hidden" name="action" value="submit_order">
-
- <?php if ($loggedInCustomer === null): ?>
- <label for="email"><?= htmlspecialchars(Language::get('checkout_email_label', 'Email address')) ?></label>
- <input type="email" name="email" id="email" required value="<?= htmlspecialchars($formValues['email']) ?>">
- <?php endif; ?>
-
- <?php if ($checkoutRequiresShipping): ?>
- <h2><?= htmlspecialchars(Language::get('checkout_shipping_title', 'Shipping address')) ?></h2>
- <label for="shipping_name"><?= htmlspecialchars(Language::get('checkout_full_name', 'Full name')) ?></label>
- <input type="text" name="shipping_name" id="shipping_name" required value="<?= htmlspecialchars($formValues['shipping_name']) ?>">
- <label for="shipping_address_line1"><?= htmlspecialchars(Language::get('checkout_address_line1', 'Address')) ?></label>
- <input type="text" name="shipping_address_line1" id="shipping_address_line1" required value="<?= htmlspecialchars($formValues['shipping_address_line1']) ?>">
- <label for="shipping_address_line2"><?= htmlspecialchars(Language::get('checkout_address_line2', 'Address line 2 (optional)')) ?></label>
- <input type="text" name="shipping_address_line2" id="shipping_address_line2" value="<?= htmlspecialchars($formValues['shipping_address_line2']) ?>">
- <label for="shipping_city"><?= htmlspecialchars(Language::get('checkout_city', 'City')) ?></label>
- <input type="text" name="shipping_city" id="shipping_city" required value="<?= htmlspecialchars($formValues['shipping_city']) ?>">
- <label for="shipping_region"><?= htmlspecialchars(Language::get('checkout_region', 'State / Region')) ?></label>
- <input type="text" name="shipping_region" id="shipping_region" value="<?= htmlspecialchars($formValues['shipping_region']) ?>">
- <label for="shipping_postal_code"><?= htmlspecialchars(Language::get('checkout_postal_code', 'Postal code')) ?></label>
- <input type="text" name="shipping_postal_code" id="shipping_postal_code" required value="<?= htmlspecialchars($formValues['shipping_postal_code']) ?>">
- <label for="shipping_country"><?= htmlspecialchars(Language::get('checkout_country', 'Country (2-letter code)')) ?></label>
- <input type="text" name="shipping_country" id="shipping_country" required maxlength="2" placeholder="US" value="<?= htmlspecialchars($formValues['shipping_country']) ?>">
-
- <label style="display:block;margin-top:12px;">
- <input type="checkbox" name="billing_same_as_shipping" value="1" id="billing_same_as_shipping" checked onchange="document.getElementById('billing-fields').style.display = this.checked ? 'none' : 'block';">
- <?= htmlspecialchars(Language::get('checkout_billing_same', 'Billing address same as shipping')) ?>
- </label>
-
- <div id="billing-fields" style="display:none;">
- <h2><?= htmlspecialchars(Language::get('checkout_billing_title', 'Billing address')) ?></h2>
- <label for="billing_name"><?= htmlspecialchars(Language::get('checkout_full_name', 'Full name')) ?></label>
- <input type="text" name="billing_name" id="billing_name" value="<?= htmlspecialchars($formValues['billing_name']) ?>">
- <label for="billing_address_line1"><?= htmlspecialchars(Language::get('checkout_address_line1', 'Address')) ?></label>
- <input type="text" name="billing_address_line1" id="billing_address_line1" value="<?= htmlspecialchars($formValues['billing_address_line1']) ?>">
- <label for="billing_address_line2"><?= htmlspecialchars(Language::get('checkout_address_line2', 'Address line 2 (optional)')) ?></label>
- <input type="text" name="billing_address_line2" id="billing_address_line2" value="<?= htmlspecialchars($formValues['billing_address_line2']) ?>">
- <label for="billing_city"><?= htmlspecialchars(Language::get('checkout_city', 'City')) ?></label>
- <input type="text" name="billing_city" id="billing_city" value="<?= htmlspecialchars($formValues['billing_city']) ?>">
- <label for="billing_region"><?= htmlspecialchars(Language::get('checkout_region', 'State / Region')) ?></label>
- <input type="text" name="billing_region" id="billing_region" value="<?= htmlspecialchars($formValues['billing_region']) ?>">
- <label for="billing_postal_code"><?= htmlspecialchars(Language::get('checkout_postal_code', 'Postal code')) ?></label>
- <input type="text" name="billing_postal_code" id="billing_postal_code" value="<?= htmlspecialchars($formValues['billing_postal_code']) ?>">
- <label for="billing_country"><?= htmlspecialchars(Language::get('checkout_country', 'Country (2-letter code)')) ?></label>
- <input type="text" name="billing_country" id="billing_country" maxlength="2" placeholder="US" value="<?= htmlspecialchars($formValues['billing_country']) ?>">
- </div>
- <?php else: ?>
- <p class="checkout-digital-note"><?= htmlspecialchars(Language::get('checkout_digital_only', 'Your order contains only digital products, so no shipping address is needed.')) ?></p>
- <?php endif; ?>
-
- <label for="customer_note"><?= htmlspecialchars(Language::get('checkout_note_label', 'Order note (optional)')) ?></label>
- <textarea name="customer_note" id="customer_note" rows="3"><?= htmlspecialchars($formValues['customer_note']) ?></textarea>
-
- <button type="submit" class="unlock-btn" style="border:none;cursor:pointer;margin-top:16px;"><?= htmlspecialchars(Language::get('checkout_pay_button', 'Continue to payment')) ?></button>
- </form>
- <?php endif; ?>
-
- <?php require __DIR__ . '/includes/front-footer.php'; ?>
-