Files
Commerce/checkout.php

1097 lines
49 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// Prevent direct access to file
defined(security_key) or exit;
// ---------------------------------------
// Defaults
// ---------------------------------------
$account = [
'account_id' => $_SESSION['account_id'] ?? '',
'email' => $_POST['email'] ?? '',
'first_name' => $_POST['first_name'] ?? '',
'last_name' => $_POST['last_name'] ?? '',
'address_street' => $_POST['address_street'] ?? '',
'address_city' => $_POST['address_city'] ?? '',
'address_state' => $_POST['address_state'] ?? '',
'address_zip' => $_POST['address_zip'] ?? '',
'address_country' => $_POST['address_country'] ?? '',
'address_phone' => $_POST['address_phone'] ?? ''
];
$products_in_cart = isset($_SESSION['cart']) ? $_SESSION['cart'] : [];
$subtotal = 0.00;
$total = 0.00;
$shippingtotal = 0.00;
$discounttotal = 0.00;
$taxtotal = 0.00;
$tax_rate = '';
$weighttotal = 0;
$shipping_methods = [];
$checkout_input = [
"selected_country" => isset($_POST['address_country']) ? $_POST['address_country'] : (isset($account['address_country']) ? $account['address_country'] : 21) ,
"selected_shipment_method" => isset($_POST['shipping_method']) ? $_POST['shipping_method'] : '',
"business_type" => 'b2c',
"discount_code" => isset($_SESSION['discount']) ? $_SESSION['discount'] : ''
];
// Error array, output errors on the form
$errors = [];
// ---------------------------------------------
// End defaults --------------------------------
// ---------------------------------------------
// Redirect the user if the shopping cart is empty
if (empty($_SESSION['cart'])) {
header('Location: ' . url('index.php?page=cart'));
exit;
}
// Check if user is logged in
if (isset($_SESSION['account_loggedin'])) {
$api_url = '/v2/identity/userkey='.$_SESSION['account_id'];
$account = ioAPIv2($api_url,'',$clientsecret);
if (!empty($account)){$account = json_decode($account,true);}
$account = $account[0];
//RESET ACCOUNT_ID
$account['account_id'] = $account['userkey'];
}
// Update discount code
if (isset($_POST['discount_code']) && !empty($_POST['discount_code'])) {
$_SESSION['discount'] = $_POST['discount_code'];
} else if (isset($_POST['discount_code']) && empty($_POST['discount_code']) && isset($_SESSION['discount'])) {
unset($_SESSION['discount']);
}
//-------------------------------
// If there are products in cart handle the checkout
//-------------------------------
if ($products_in_cart) {
// First, calculate cart to get initial totals (without shipping)
$initial_payload = json_encode(array("cart" => $products_in_cart, "checkout_input" => $checkout_input), JSON_UNESCAPED_UNICODE);
$initial_cart = ioAPIv2('/v2/checkout/',$initial_payload,$clientsecret);
$initial_cart = json_decode($initial_cart,true);
// Get initial totals for shipping method calculation
$initial_subtotal = $initial_cart['totals']['subtotal'] ?? 0;
$initial_weighttotal = $initial_cart['totals']['weighttotal'] ?? 0;
// Now retrieve shipping methods with correct totals
$shipping_methods = ioAPIv2('/v2/shipping/list=methods&country='.$checkout_input['selected_country'].'&price_total='.$initial_subtotal.'&weight_total='.$initial_weighttotal,'',$clientsecret);
$shipping_methods = json_decode($shipping_methods,true);
// If no shipping method is selected and shipping methods are available, select the first one as default
if (empty($checkout_input['selected_shipment_method']) && !empty($shipping_methods) && count($shipping_methods) > 0) {
$checkout_input['selected_shipment_method'] = $shipping_methods[0]['id'];
}
// Recalculate shopping_cart with selected shipping method
$payload = json_encode(array("cart" => $products_in_cart, "checkout_input" => $checkout_input), JSON_UNESCAPED_UNICODE);
$products_in_cart = ioAPIv2('/v2/checkout/',$payload,$clientsecret);
$products_in_cart = json_decode($products_in_cart,true);
//GET SPECIFIC TOTALS FROM API RESULTS
$subtotal = $products_in_cart['totals']['subtotal'];
$shippingtotal = $products_in_cart['totals']['shippingtotal'];
$discounttotal = $products_in_cart['totals']['discounttotal'];
$taxtotal = $products_in_cart['totals']['taxtotal'];
$tax_rate = $products_in_cart['totals']['tax_rate'];
$weighttotal = $products_in_cart['totals']['weighttotal'];
$total = $products_in_cart['totals']['total'];
// Redirect the user if the shopping cart is empty
if (empty($products_in_cart)) {
header('Location: ' . url('index.php?page=cart'));
exit;
}
//-------------------------------
// END Checkout handler
//-------------------------------
}
//-------------------------------
//Place order
//-------------------------------
// Make sure when the user submits the form all data was submitted and shopping cart is not empty
if (isset($_POST['method'], $_POST['first_name'], $_POST['last_name'], $_POST['address_street'], $_POST['address_city'], $_POST['address_state'], $_POST['address_zip'], $_POST['address_country'], $_SESSION['cart']) && !isset($_POST['update'])) {
$account_id = null;
// If the user is already logged in
if (isset($_SESSION['account_loggedin'])) {
// Account logged-in, update the user's details
$payload = json_encode(
array(
"language" => $_SESSION['country_code'],
"first_name" => $_POST['first_name'],
"last_name" => $_POST['last_name'],
"address_street" => $_POST['address_street'],
"address_city" => $_POST['address_city'],
"address_state" => $_POST['address_state'],
"address_zip" => $_POST['address_zip'],
"address_country" => $_POST['address_country'],
"address_phone" => $_POST['address_phone'] ?? '',
"userkey" => $_SESSION['account_id']), JSON_UNESCAPED_UNICODE);
$account_update = ioAPIv2('/v2/identity/',$payload,$clientsecret);
$account_update = json_decode($account_update,true);
$account_id = $account['account_id'] = $_SESSION['account_id'];
} else if (isset($_POST['email'], $_POST['password'], $_POST['cpassword']) && filter_var($_POST['email'], FILTER_VALIDATE_EMAIL) && !empty($_POST['password']) && !empty($_POST['cpassword'])) {
// User is not logged in, check if the account already exists with the email they submitted
// Check if the account exists
$account = ioAPIv2('/v2/identity/email='.$_POST['email'],'',$clientsecret);
$account = json_decode($account,true);
if ($account) {
// Email exists, user should login instead...
$errors[] = $error_account_name;
}
if (strlen($_POST['password']) > 20 || strlen($_POST['password']) < 5) {
// Password must be between 5 and 20 characters long.
$errors[] = $error_account_password_rules;
}
if ($_POST['password'] != $_POST['cpassword']) {
// Password and confirm password fields do not match...
$errors[] = $error_account_password_match;
}
if (!$errors) {
// Account doesnt exist, create new account
$payload = json_encode(
array(
"email" => $_POST['email'],
"password" => $_POST['password'],
"language" => $_SESSION['country_code'],
"first_name" => $_POST['first_name'],
"last_name" => $_POST['last_name'],
"address_street" => $_POST['address_street'],
"address_city" => $_POST['address_city'],
"address_state" => $_POST['address_state'],
"address_zip" => $_POST['address_zip'],
"address_country" => $_POST['address_country'],
"address_phone" => $_POST['address_phone'] ?? ''), JSON_UNESCAPED_UNICODE);
$account = ioAPIv2('/v2/identity/',$payload,$clientsecret);
$account= json_decode($account,true);
$account_id = $account['account_id'] = $account['accountID'];
if ($account && isset($account['accountID'])) {
//SEND VERIFICATION EMAIL
include dirname(__FILE__).'/custom/email/email_template_register.php';
$register_mail = $message;
send_mail_by_PHPMailer($account['identity'], $subject, $register_mail,'', '');
$register_error = 'Email send to verify your account';
}
}
} else if (account_required) {
$errors[] = $error_account;
}
if (!$errors && $products_in_cart) {
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//Process checkout => add payment_method to checkout_input array
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
$checkout_input['payment_method'] = $_POST['method'];
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Calculate shopping_cart based on session
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
$payload = json_encode(array("cart" => $_SESSION['cart'], "checkout_input" => $checkout_input, "customer_details" => $account), JSON_UNESCAPED_UNICODE);
$place_order = ioAPIv2('/v2/placeorder/',$payload,$clientsecret);
$place_order = json_decode($place_order,true);
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//Check if transaction is succesfull and send order confirmation to customer
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($place_order['error'] == '' && $place_order['id'] != ''){
// Push purchase event to dataLayer
if (isset($place_order['products_checked-out']) && is_array($place_order['products_checked-out'])) {
$productIds = [];
$totalQuantity = 0;
$products = [];
foreach ($place_order['products_checked-out'] as $p) {
if (is_array($p)) {
$productIds[] = $p['id'] ?? '';
$totalQuantity += $p['quantity'] ?? 0;
$products[] = [
'id' => $p['id'] ?? '',
'name' => $p['meta']['name'] ?? '',
'price' => $p['options_price'] ?? 0,
'quantity' => $p['quantity'] ?? 0
];
}
}
echo "<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'purchase',
'ecommerce': {
'purchase': {
'actionField': {
'id': '" . $place_order['transaction_id'] . "',
'revenue': '" . $place_order['payment_amount'] . "',
'tax': '" . $place_order['taxtotal'] . "',
'shipping': '" . $place_order['shippingtotal'] . "'
},
'products': " . json_encode($products) . "
}
},
'content_type': 'product',
'content_ids': " . json_encode($productIds) . ",
'value': " . floatval($place_order['payment_amount']) . ",
'currency': 'EUR',
'num_items': " . $totalQuantity . "
});
</script>";
}
// Email will be sent by webhook after successful payment
//Disable giftcard
if (isset($_SESSION['discount'])){
if (preg_match("/[#][0-9]/", $_SESSION['discount']) == 1){
useGiftCart($pdo, $_SESSION['discount']);
}
}
// Authenticate the user
if ($account_id != null) {
// Log the user in with the details provided
session_regenerate_id();
$_SESSION['account_loggedin'] = TRUE;
$_SESSION['account_id'] = $account_id;
$_SESSION['account_role'] = $account ? $account['profile'] : 0;
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//Pay on delivery = 2
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
if (pay_on_delivery_enabled && $place_order['payment_method'] == 2){
header('Location: ' . url('index.php?page=placeorder'));
exit;
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Mollie = 3 ++++++++++++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
if (mollie_enabled && $_POST['method'] == 3) {
try {
/*
* Initialize the Mollie API library with your API key.
*
* See: https://www.mollie.com/dashboard/developers/api-keys
*/
require "initialize.php";
/*
* Generate a unique order id for this example. It is important to include this unique attribute
* in the redirectUrl (below) so a proper return page can be shown to the customer.
*/
$orderId = $place_order['transaction_id'];
$value = number_format($place_order['payment_amount'],2,'.','');
/*
* Determine the url parts to these example files.
*/
$protocol = isset($_SERVER['HTTPS']) && strcasecmp('off', $_SERVER['HTTPS']) !== 0 ? "https" : "http";
$hostname = $_SERVER['HTTP_HOST'];
$path = dirname($_SERVER['REQUEST_URI'] ?? $_SERVER['PHP_SELF']);
/*
* Payment parameters:
* amount Amount in EUROs.
* description Description of the payment.
* redirectUrl Redirect location. The customer will be redirected there after the payment.
* webhookUrl Webhook location, used to report when the payment changes state.
* metadata Custom metadata that is stored with the payment.
*/
if (rewrite_url){
$redirectURL = $protocol.'://'.$hostname.$path.'placeorder/'.$orderId;
}else{
$redirectURL = $protocol.'://'.$hostname.$path.'index.php?page=placeorder&order_id='.$orderId;
}
$payment = $mollie->payments->create([
"amount" => [
"currency" => "EUR",
"value" => "{$value}", // You must send the correct number of decimals, thus we enforce the use of strings
],
"description" => "Order #{$orderId}",
"redirectUrl" => "$redirectURL",
"webhookUrl" => "{$protocol}://{$hostname}{$path}webhook.php",
"metadata" => [
"order_id" => $orderId,
],
]);
/*
* Send the customer off to complete the payment.
* This request should always be a GET, thus we enforce 303 http response code
*/
// Send customer to checkout
header("Location: " . $payment->getCheckoutUrl(), true, 303);
} catch (\Mollie\Api\Exceptions\ApiException $e) {
echo "API call failed: " . htmlspecialchars($e->getMessage());
}
exit;
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// PayPal Payment = 1 +++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
if (paypal_enabled && $_POST['method'] == 1) {
//Process Payment
require_once __DIR__."/lib/paypal/paypal.php";
$base = PAYPAL_URL;
$id = PAYPAL_CLIENT_ID;
$secret = PAYPAL_CLIENT_SECRET;
//init input
$order = $place_order['transaction_id'];
$price = number_format($place_order['payment_amount'],2,'.','');
$currency = "EUR";
//make payment
$paypal = new paypalCurl();
$paypal->init($id,$secret,$base);
$result = $paypal->makePaymentURL($order,$price,$currency);
if ($result->status === true) {
header("location:". $result->url);
die;
}
else { //raise error
echo $result->msg;
die;
}
}
} else {
foreach ($place_order['error'] as $error){
$errors[] = $error;
}
}
}
}
//-------------------------------
// END PLACE ORDER
//-------------------------------
$terms_link = url('index.php?page=termsandconditions');
$privacy_link = url('index.php?page=privacy');
$view = template_header(($checkout_header ?? 'Checkout'),'');
$view .= '
<div class="checkout checkout-wizard">
<h1>'.($h1_Checkout ?? 'Checkout').'</h1>
<!-- Progress Steps -->
<div class="progress-steps">
<div class="progress-step active" data-step="1">
<div class="step-number">1</div>
<div class="step-label">'.($step_contact ?? 'Contact').'</div>
</div>
<div class="progress-step" data-step="2">
<div class="step-number">2</div>
<div class="step-label">'.($step_payment ?? 'Payment').'</div>
</div>
<div class="progress-step" data-step="3">
<div class="step-number">3</div>
<div class="step-label">'.($step_shipping ?? 'Shipping').'</div>
</div>
<div class="progress-step" data-step="4">
<div class="step-number">4</div>
<div class="step-label">'.($step_review ?? 'Review').'</div>
</div>
</div>';
if ($errors) {
foreach($errors as $error) {
$view .= '<p class="error">'.$error.'</p>';
}
}
$view .= '
<form id="checkout-form" method="post" autocomplete="on">
<div class="container">
<div class="checkout-steps">
<!-- STEP 1: Contact Information -->
<div class="step-section active" id="step-1" data-step="1">
<div class="step-header" onclick="editStep(1)">
<div class="step-header-content">
<div class="step-badge">1</div>
<h2 class="step-title">'.($step_contact ?? 'Contact Information').'</h2>
</div>
<div class="step-edit">'.($edit_text ?? 'Edit').'</div>
</div>
<div class="step-content">';
if (!isset($_SESSION['account_loggedin'])) {
$view .= '
<label for="email">'.($customer_email ?? 'Email').' *</label>
<input type="email" name="email" id="email" placeholder="you@example.com" class="form-field" required>
<div class="collapsible-section" style="margin-top: 1.5rem;">
<button type="button" class="collapsible-header" onclick="toggleCollapsible(this)">
<span>'.$account_create.((!account_required) ? ' '.($account_optional ?? '(Optional)') : '').'</span>
<svg class="collapsible-icon" width="20" height="20" viewBox="0 0 20 20">
<path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>
</button>
<div class="collapsible-content">
<label for="password">'.$account_create_password.'</label>
<input type="password" name="password" id="password" placeholder="'.$account_create_password.'" class="form-field" autocomplete="new-password">
<label for="cpassword">'.$account_create_password_confirm.'</label>
<input type="password" name="cpassword" id="cpassword" placeholder="'.$account_create_password_confirm.'" class="form-field" autocomplete="new-password">
</div>
</div>';
} else {
$view .= '
<label for="email_display">'.($customer_email ?? 'Email').'</label>
<input type="email" id="email_display" value="'.htmlspecialchars($account['email'], ENT_QUOTES).'" class="form-field" readonly style="background-color: #f3f4f6;">
<input type="hidden" name="email" value="'.htmlspecialchars($account['email'], ENT_QUOTES).'">';
}
$view .= '
<div class="step-buttons">
<button type="button" class="btn-continue" onclick="goToStep(2)">
'.($continue_text ?? 'Continue to Payment').'
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M7.5 15L12.5 10L7.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
</div>
<!-- STEP 2: Payment Method -->
<div class="step-section" id="step-2" data-step="2">
<div class="step-header" onclick="editStep(2)">
<div class="step-header-content">
<div class="step-badge">2</div>
<h2 class="step-title">'.$payment_method.'</h2>
</div>
<div class="step-edit">'.($edit_text ?? 'Edit').'</div>
</div>
<div class="step-content">
<div class="payment-methods-grid">';
if (mollie_enabled){
$view .= ' <div class="payment-option">
<input id="mollie-ideal" type="radio" name="method" value="3" '. ((mollie_default)? 'checked':'') .'>
<label for="mollie-ideal" class="payment-card">
<div class="payment-card-content">
<div class="payment-logos">
<img src="./custom/assets/wero.svg" alt="Bank Transfer" class="payment-logo" style="width:120px">
</div>
<span class="payment-name">'.($bank_transfer ?? 'Bank Transfer').'</span>
</div>
<div class="payment-check">✓</div>
</label>
</div>
<div class="payment-option">
<input id="mollie-card" type="radio" name="method" value="3">
<label for="mollie-card" class="payment-card">
<div class="payment-card-content">
<div class="payment-logos">
<img src="./custom/assets/mastercard.png" alt="Mastercard" class="payment-logo">
<img src="./custom/assets/visa.png" alt="Visa" class="payment-logo">
</div>
<span class="payment-name">'.($card_payment ?? 'Credit / Debit Card').'</span>
</div>
<div class="payment-check">✓</div>
</label>
</div>';
}
if (paypal_enabled){
$view .= ' <div class="payment-option">
<input id="paypal" type="radio" name="method" value="1" '. ((paypal_default)? 'checked':'') .'>
<label for="paypal" class="payment-card">
<div class="payment-card-content">
<div class="payment-logos">
<img src="https://www.paypalobjects.com/webstatic/mktg/Logo/pp-logo-100px.png" alt="PayPal" class="payment-logo">
</div>
<span class="payment-name">PayPal</span>
</div>
<div class="payment-check">✓</div>
</label>
</div>';
}
if (pay_on_delivery_enabled){
$view .= ' <div class="payment-option">
<input id="payondelivery" type="radio" name="method" value="2" '. ((pay_on_delivery_default)? 'checked':'') .'>
<label for="payondelivery" class="payment-card">
<div class="payment-card-content">
<div class="payment-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
</svg>
</div>
<span class="payment-name">'.$payment_method_2.'</span>
</div>
<div class="payment-check">✓</div>
</label>
</div>';
}
$view .= ' </div>
<div class="step-buttons">
<button type="button" class="btn-back" onclick="goToStep(1)">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M12.5 15L7.5 10L12.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
'.($back_text ?? 'Back').'
</button>
<button type="button" class="btn-continue" onclick="goToStep(3)">
'.($continue_text ?? 'Continue to Shipping').'
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M7.5 15L12.5 10L7.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
</div>
<!-- STEP 3: Shipping Details -->
<div class="step-section" id="step-3" data-step="3">
<div class="step-header" onclick="editStep(3)">
<div class="step-header-content">
<div class="step-badge">3</div>
<h2 class="step-title">'.$h2_Shipping_details.'</h2>
</div>
<div class="step-edit">'.($edit_text ?? 'Edit').'</div>
</div>
<div class="step-content">
<div class="form-row">
<div class="form-col">
<label for="first_name">'.$shipping_first_name.' *</label>
<input type="text" value="'.htmlspecialchars($account['first_name'], ENT_QUOTES).'" name="first_name" id="first_name" placeholder="'.$shipping_first_name.'" class="form-field" required>
</div>
<div class="form-col">
<label for="last_name">'.$shipping_last_name.' *</label>
<input type="text" value="'.htmlspecialchars($account['last_name'], ENT_QUOTES).'" name="last_name" id="last_name" placeholder="'.$shipping_last_name.'" class="form-field" required>
</div>
</div>
<label for="address_street">'.$shipping_address.' *</label>
<input type="text" value="'.htmlspecialchars($account['address_street'], ENT_QUOTES).'" name="address_street" id="address_street" placeholder="'.$shipping_address.'" class="form-field" required>
<div class="form-row">
<div class="form-col">
<label for="address_city">'.$shipping_city.' *</label>
<input type="text" value="'.htmlspecialchars($account['address_city'], ENT_QUOTES).'" name="address_city" id="address_city" placeholder="'.$shipping_city.'" class="form-field" required>
</div>
<div class="form-col">
<label for="address_zip">'.$shipping_zip.' *</label>
<input type="text" value="'.htmlspecialchars($account['address_zip'], ENT_QUOTES).'" name="address_zip" id="address_zip" placeholder="'.$shipping_zip.'" class="form-field" required>
</div>
</div>
<div class="form-row">
<div class="form-col">
<label for="address_state">'.$shipping_state.'</label>
<input type="text" value="'.htmlspecialchars($account['address_state'], ENT_QUOTES).'" name="address_state" id="address_state" placeholder="'.$shipping_state.'" class="form-field">
</div>
<div class="form-col">
<label for="address_country">'.$shipping_country.' *</label>
<select name="address_country" class="ajax-update form-field" required>';
foreach($countries_in_scope as $key => $value){
$view .= ' <option value="'.$key.'" '.($key==(isset($_SESSION['account_loggedin']) ? $account['address_country'] : 21) ? ' selected' : '').'>'.(${$value} ?? $value).'</option>';
}
$view .= ' </select>
</div>
</div>
<label for="address_phone">'.$shipping_phone.'</label>
<input type="tel" value="'.htmlspecialchars(($account['address_phone'] ?? ''), ENT_QUOTES).'" name="address_phone" id="address_phone" placeholder="'.$shipping_phone.'" class="form-field">
<div class="step-buttons">
<button type="button" class="btn-back" onclick="goToStep(2)">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M12.5 15L7.5 10L12.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
'.($back_text ?? 'Back').'
</button>
<button type="button" class="btn-continue" onclick="goToStep(4)">
'.($continue_text ?? 'Continue to Review').'
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M7.5 15L12.5 10L7.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
</div>
<!-- STEP 4: Order Review -->
<div class="step-section" id="step-4" data-step="4">
<div class="step-header" onclick="editStep(4)">
<div class="step-header-content">
<div class="step-badge">4</div>
<h2 class="step-title">'.($step_review ?? 'Review Order').'</h2>
</div>
<div class="step-edit">'.($edit_text ?? 'Edit').'</div>
</div>
<div class="step-content">
<h2>'.$h2_shoppingcart.'</h2>
<table class="cart-table">';
foreach($products_in_cart['cart_details']['products'] as $product){
$view .= ' <tr>
<td class="cart-img"><img src="'.img_url.$product['meta']['img'].'" width="50" height="50" alt="'.$product['meta']['name'].'"></td>
<td class="cart-info">'.$product['quantity'].' × '.$product['meta']['name'].'</td>
<td class="cart-price">'.currency_code.''.number_format($product['options_price'] * $product['quantity'],2).'</td>
</tr>';
}
$view .= ' </table>
<div class="collapsible-section discount-section">
<button type="button" class="collapsible-header discount-header" onclick="toggleCollapsible(this)">
<span>'.$discount_label.'</span>
<svg class="collapsible-icon" width="20" height="20" viewBox="0 0 20 20">
<path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>
</button>
<div class="collapsible-content">
<div class="discount-input-wrapper">
<input type="text" class="ajax-update form-field" name="discount_code" placeholder="'.$discount_label.'" value="'.(isset($_SESSION['discount']) ? $_SESSION['discount'] : '').'">
</div>
<span class="discount-result">';
if (isset($_SESSION['discount'], $products_in_cart['totals']['discounttotal'])){
$view .= $products_in_cart['totals']['discount_message'];
}
$view .= ' </span>
</div>
</div>
<div class="shipping-methods-wrapper" id="shipping-method-section">';
if (isset($shipping_methods) && count($shipping_methods) > 0){
$view .= ' <h3 class="shipping-title">'.$h3_shipping_method.' *</h3>
<div class="shipping-methods">';
foreach($shipping_methods as $method){
$view .= ' <label class="shipping-method">
<input type="radio" class="ajax-update" id="sm'.$method['id'].'" name="shipping_method" value="'.$method['id'].'" required'.(($checkout_input['selected_shipment_method']==$method['id'] || count($shipping_methods) == 1) ? ' checked':'').'>
<span class="shipping-info">
<span class="shipping-name">'.$method['name'].'</span>
<span class="shipping-price">'.currency_code.''.number_format($method['price'], 2).'</span>
</span>
</label>';
}
$view .= ' </div>';
}
$view .= ' </div>
<div class="order-summary">
<div class="summary-row">
<span>'.$total_subtotal.'</span>
<span class="summary-value">'.currency_code.''.number_format($subtotal,2).'</span>
</div>
<div class="summary-row">
<span>'.$total_shipping.'</span>
<span class="summary-value">'.currency_code.''.number_format($shippingtotal,2).'</span>
</div>';
if ($discounttotal > 0){
$view .= ' <div class="summary-row discount-row">
<span>'.$total_discount.'</span>
<span class="summary-value">-'.currency_code.''.number_format(round($discounttotal, 1),2).'</span>
</div>';
}
if ($taxtotal > 0){
$view .= ' <div class="summary-row">
<span>'.($tax_text ?? 'VAT').' <span class="tax-rate">('.$tax_rate.')</span></span>
<span class="summary-value">'.currency_code.''.number_format($taxtotal,2).'</span>
</div>';
}
$view .= ' </div>
<div class="order-total">
<span class="total-label">'.$total_total.' <span class="total-note">'.$total_total_note.'</span></span>
<span class="total-amount">'.currency_code.''.number_format($total,2).'</span>
</div>
<div class="consent-section" id="consent-section" style="">
<label class="consent-checkbox">
<input type="checkbox" id="consent_comms" name="consent_comms" value="1">
<span class="consent-text">'.$order_consent_1.'</span>
</label>
<label class="consent-checkbox required">
<input type="checkbox" id="consent" name="consent" value="1" required>
<span class="consent-text">'.$order_consent_2.' * <a href="'.$terms_link.'" target="_blank">'.$order_consent_3.'</a> '.$order_consent_4.' <a href="'.$privacy_link.'" target="_blank">'.$order_consent_5.'</a></span>
</label>
</div>
<button type="submit" name="checkout" class="checkout-btn" id="final-checkout-btn">
<span>'.$btn_place_order.'</span>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M7.5 15L12.5 10L7.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="step-buttons">
<button type="button" class="btn-back" onclick="goToStep(3)">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M12.5 15L7.5 10L12.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
'.($back_text ?? 'Back').'
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
';
echo '<style>
/* Sticky step buttons for mobile - CSS solution like cart.php and product.php */
.step-buttons {
transition: all 0.3s ease;
}
@media (max-width: 768px) {
.step-buttons {
position: fixed !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100% !important;
z-index: 1000 !important;
background: #fff !important;
border-radius: 0 !important;
margin: 0 !important;
padding: 1rem !important;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.15) !important;
display: flex !important;
gap: 0.5rem !important;
}
/* Add padding at bottom for sticky buttons */
.step-section.active .step-content {
padding-bottom: 80px;
}
/* Ensure buttons take full width on mobile */
.step-buttons .btn-back,
.step-buttons .btn-continue {
flex: 1;
}
/* When only one button on step 1, make container and button full orange background */
#step-1 .step-buttons {
background: var(--color-primary) !important;
padding: 0 !important;
}
#step-1 .step-buttons .btn-continue {
background-color: var(--color-primary) !important;
color: white !important;
border: none !important;
padding: 1rem !important;
margin: 0 !important;
}
/* Add scroll margin to step sections for mobile */
.step-section {
scroll-margin-top: 20px;
}
.progress-steps {
position: relative;
z-index: 1;
}
/* Make checkout button sticky on step 4 */
#step-4 .checkout-btn {
position: fixed !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100% !important;
z-index: 1000 !important;
border-radius: 0 !important;
margin: 0 !important;
padding: 1rem !important;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.15) !important;
background-color: var(--color-primary) !important;
color: white !important;
border: none !important;
}
/* Hide step-buttons on step 4 since checkout button is sticky */
#step-4 .step-buttons {
display: none !important;
}
}
</style>
<script>
let currentStep = 1;
let maxStepReached = 1;
function goToStep(step) {
// Only validate if moving forward to a new step
if (step > maxStepReached && !validateStep(currentStep)) {
return;
}
// Save current step data
saveStepSummary(currentStep);
// Update max step reached
if (step > maxStepReached) {
maxStepReached = step;
}
// Hide all steps
document.querySelectorAll(".step-section").forEach(s => {
s.classList.remove("active");
});
// Mark all steps up to maxStepReached as completed
document.querySelectorAll(".step-section").forEach((s, i) => {
if (i < maxStepReached - 1) {
s.classList.add("completed");
} else if (i === maxStepReached - 1) {
s.classList.remove("completed");
}
});
// Show target step
const targetStep = document.getElementById("step-" + step);
if (targetStep) {
targetStep.classList.add("active");
targetStep.classList.remove("completed");
}
// Update progress
document.querySelectorAll(".progress-step").forEach((s, i) => {
if (i < step - 1) {
s.classList.add("completed");
s.classList.remove("active");
} else if (i === step - 1) {
s.classList.add("active");
s.classList.remove("completed");
} else if (i < maxStepReached) {
s.classList.add("completed");
} else {
s.classList.remove("active", "completed");
}
});
currentStep = step;
// Scroll to guide user through the step
setTimeout(() => {
const targetStep = document.getElementById("step-" + step);
if (targetStep) {
// Close any active keyboard
if (document.activeElement) {
document.activeElement.blur();
}
// Scroll with explicit position calculation
const rect = targetStep.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const offset = 10; // Small offset from top
window.scrollTo({
top: scrollTop + rect.top - offset,
behavior: "smooth"
});
}
}, 300);
}
function editStep(step) {
// Allow navigating to any step that has been reached
if (step <= maxStepReached) {
goToStep(step);
}
}
function validateStep(step) {
const stepElement = document.getElementById("step-" + step);
const inputs = stepElement.querySelectorAll("input[required], select[required]");
// Remove previous error highlights
stepElement.querySelectorAll(".error-highlight").forEach(el => {
el.classList.remove("error-highlight");
});
let firstInvalidField = null;
let hasErrors = false;
for (let input of inputs) {
if (!input.checkValidity()) {
// Add visual highlight for Step 3
if (step === 3) {
input.classList.add("error-highlight");
if (!firstInvalidField) {
firstInvalidField = input;
}
hasErrors = true;
} else {
input.reportValidity();
return false;
}
}
}
// For Step 3, scroll to first error and show validation message
if (step === 3 && hasErrors) {
if (firstInvalidField) {
firstInvalidField.scrollIntoView({ behavior: "smooth", block: "center" });
firstInvalidField.reportValidity();
}
return false;
}
// Additional validation for payment method
if (step === 2) {
const paymentSelected = document.querySelector("input[name=\"method\"]:checked");
if (!paymentSelected) {
alert("'.($select_payment ?? 'Please select a payment method').'");
return false;
}
}
return true;
}
function saveStepSummary(step) {
// Step summaries removed - validation handled by form
}
function toggleCollapsible(button) {
const section = button.closest(".collapsible-section");
const content = section.querySelector(".collapsible-content");
const icon = button.querySelector(".collapsible-icon");
section.classList.toggle("active");
if (section.classList.contains("active")) {
content.style.maxHeight = content.scrollHeight + "px";
icon.style.transform = "rotate(180deg)";
} else {
content.style.maxHeight = "0";
icon.style.transform = "rotate(0deg)";
}
}
// Auto-expand discount if already applied
window.addEventListener("DOMContentLoaded", function() {
const discountValue = document.querySelector("input[name=\"discount_code\"]").value;
if (discountValue) {
const discountButton = document.querySelector(".discount-header");
if (discountButton) {
toggleCollapsible(discountButton);
}
}
// Step 1: Enter key on email field
const emailField = document.getElementById("email");
if (emailField) {
emailField.addEventListener("keypress", function(e) {
if (e.key === "Enter") {
e.preventDefault();
goToStep(2);
}
});
}
// Step 2: Auto-advance after payment method selection
const paymentMethods = document.querySelectorAll("input[name=\"method\"]");
paymentMethods.forEach(function(radio) {
radio.addEventListener("change", function() {
setTimeout(function() {
goToStep(3);
}, 300);
});
});
// Form submission validation
const checkoutForm = document.getElementById("checkout-form");
if (checkoutForm) {
checkoutForm.addEventListener("submit", function(e) {
// Ensure user is on step 4
if (currentStep !== 4) {
e.preventDefault();
alert("Please complete all steps before placing your order.");
return false;
}
// Validate all required fields exist
const requiredFields = ["method", "first_name", "last_name", "address_street", "address_city", "address_zip", "address_country"];
for (let field of requiredFields) {
const input = checkoutForm.querySelector("[name=\"" + field + "\"]");
if (!input || !input.value) {
e.preventDefault();
alert("Please complete all required fields.");
return false;
}
}
// Validate consent checkbox
const consentCheckbox = document.getElementById("consent");
if (consentCheckbox && !consentCheckbox.checked) {
e.preventDefault();
consentCheckbox.classList.add("error-highlight");
consentCheckbox.scrollIntoView({ behavior: "smooth", block: "center" });
setTimeout(() => {
consentCheckbox.reportValidity();
}, 300);
return false;
}
});
}
});
</script>
';
$view .= template_footer();
//OUTPUT
echo $view;
?>