Stripe Facebook issue debug

User
I have had a difficult weekend trying to debug issues on the Stripe Checkout. Visitors coming to the site from Facebook could not enter any info in the Credit Card fields.
I have integrated stripe like this
let orderData = {
tickets: [],
addOns: [],
total: 0
};

// Store event date string globally for use in order summary
let selectedEventDateString = '';

// Global add-ons data for label lookup
let globalAddOnsData = [];

// Cache DOM elements
const domCache = {
form: null,
ticketSections: null,
addOnsGrid: null,
cart: null
};

// Initialize DOM cache
function initializeDomCache() {
domCache.form = document.getElementById('event-order-form');
domCache.ticketSections = document.getElementById('ticket-sections');
domCache.addOnsGrid = document.getElementById('add-ons-grid');
domCache.cart = document.getElementById('cart');
}

function updateOrderSummary() {
console.log('updateOrderSummary called');
console.log('Current orderData:', orderData);

if (!domCache.form) return;

// Update event date/time in order summary (step 3)
const summaryEventDate = document.getElementById('summary-event-date');
if (summaryEventDate) {
summaryEventDate.textContent = selectedEventDateString || '';
}

let subtotal = 0;
let ticketData = [];

// Process each ticket
orderData.tickets.forEach(ticket => {
if (ticket.quantity > 0) {
const itemSubtotal = ticket.quantity * ticket.price;
subtotal += itemSubtotal;
ticketData.push({
section: ticket.section,
type: ticket.type,
quantity: ticket.quantity,
price: ticket.price,
subtotal: itemSubtotal
});
}
});

// Calculate add-ons total
const addOnsTotal = calculateAddOns();
let total = subtotal + addOnsTotal;

// Apply voucher discount if valid – only to tickets
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const voucherMatch = voucherText.match(/Voucher Applied: (w+)/);
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (voucherMatch && discountMatch) {
const voucherCode = voucherMatch[1];
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];

if (discountType === '%') {
// Apply discount only to ticket subtotal
const discountedTicketTotal = subtotal * (1 – discountValue / 100);
total = discountedTicketTotal + addOnsTotal;
} else {
// For fixed amount, apply to ticket total only
const discountedTicketTotal = Math.max(0, subtotal – discountValue);
total = discountedTicketTotal + addOnsTotal;
}
}
}

// Update cart display
updateCartDisplay(ticketData, subtotal, addOnsTotal, total);
saveOrderDataToStorage();
addRemoveListenersToSummary();

// Update Express Checkout amount if available
if (window.updatePaymentRequestAmount) {
window.updatePaymentRequestAmount();
}
}

function calculateSubtotal() {
let subtotal = 0;
const ticketSelects = document.querySelectorAll('select[name^="event_seat_"]');

for (const select of ticketSelects) {
subtotal += parseInt(select.value) * parseFloat(select.dataset.price);
}

return subtotal;
}

function calculateAddOns() {
let addOnsTotal = 0;
orderData.addOns.forEach(addon => {
addOnsTotal += addon.quantity * addon.price;
});
return addOnsTotal;
}

function generateOrderSummaryHTML() {
let html = '';

// Add tickets to summary
if (orderData?.tickets?.length > 0) {
html += orderData.tickets
.filter(ticket => ticket?.type && ticket?.price && ticket?.quantity)
.map(ticket => `

${ticket.type} Ticket
€${ticket.price.toFixed(2)} each

x${ticket.quantity}
€${(ticket.price * ticket.quantity).toFixed(2)}

`).join('');
}

// Add wheelchair count to summary
const wheelchairUsers = getWheelchairUserData();
if (wheelchairUsers > 0) {
html += `

Wheelchair
Access required

x${wheelchairUsers}

`;
}

// Add add-ons to summary
if (orderData?.addOns?.length > 0) {
console.log('Generating add-ons summary for:', orderData.addOns);
html += orderData.addOns
.filter(addon => addon?.name && addon?.price && addon?.quantity)
.map(addon => {
console.log('Processing addon:', addon);
const displayName = addon.label || addon.name;
console.log('Display name:', displayName);
return `

${displayName}
€${addon.price.toFixed(2)} each

x${addon.quantity}
€${(addon.price * addon.quantity).toFixed(2)}

`;
}).join('');
}

// If cart is empty, show message
if (!orderData?.tickets?.length && !orderData?.addOns?.length && wheelchairUsers === 0) {
html = `

Your cart is empty
Select tickets to continue

`;
} else {
// Add total if there are items
const total = orderData.total || 0;
html += `

Total:
€${total.toFixed(2)}

`;
}

return html;
}

function saveOrderData(orderData) {
localStorage.setItem('orderData', JSON.stringify(orderData));

if (domCache.form) {
// Update ticket quantities
if (Array.isArray(orderData.tickets)) {
orderData.tickets.forEach(ticket => {
const select = domCache.form.querySelector(`[name="event_seat_${ticket.type.toLowerCase()}"]`);
if (select) select.value = ticket.quantity;
});
}

// Update add-ons
if (Array.isArray(orderData.addOns)) {
orderData.addOns.forEach(addon => {
const select = domCache.form.querySelector(`[name="addon_${addon.name}"]`);
if (select) select.value = addon.quantity;
});
}
}

// Update cart display without triggering saveOrderData
if (Array.isArray(orderData.tickets)) {
updateCartDisplay(
orderData.tickets,
orderData.total – (orderData.addOns?.reduce((sum, addon) => sum + (addon.price * addon.quantity), 0) || 0),
orderData.addOns?.reduce((sum, addon) => sum + (addon.price * addon.quantity), 0) || 0,
orderData.total,
false
);
}
}

// Add this function near the top-level functions
function saveOrderDataToStorage() {
try {
localStorage.setItem('fossettsOrderData', JSON.stringify(orderData));
} catch (e) {
console.error('Failed to save order data:', e);
}
}

function restoreSelectionsToUI() {
// Restore ticket quantities
if (orderData.tickets && Array.isArray(orderData.tickets)) {
orderData.tickets.forEach(ticket => {
// Use lowercase for ticket type to match attribute
const input = document.querySelector(`.quantity-value[data-section="${ticket.section}"][data-ticket-type="${ticket.type.toLowerCase()}"]`);
if (input) {
input.value = ticket.quantity;
}
});
}
// Restore add-on quantities
if (orderData.addOns && Array.isArray(orderData.addOns)) {
orderData.addOns.forEach(addon => {
const input = document.querySelector(`.quantity-value[data-addon-name="${addon.name}"]`);
if (input) {
input.value = addon.quantity;
}
});
}
}

// On DOMContentLoaded, load orderData from localStorage if present
const savedOrderData = localStorage.getItem('fossettsOrderData');
if (savedOrderData) {
try {
orderData = JSON.parse(savedOrderData);
restoreSelectionsToUI();
updateOrderSummary();
// Optionally, update UI for tickets/addons here if needed
} catch (e) {
console.error('Failed to parse saved order data:', e);
}
}

// Global function to show speech bubble warning
function showSpeechBubble(message) {
console.log('Showing speech bubble:', message);
const speechBubble = document.querySelector('.speech-bubble-warning');
if (!speechBubble) {
console.error('Speech bubble element not found');
return;
}

// Determine which step is active and position accordingly
const activeStep = document.querySelector('.step_1.active, .step_2.active, .step_3.active');
let nextButton = document.querySelector('.sticky-cart-bar .next-step');

// For Step 3, target the Buy Now button if it exists
if (activeStep && activeStep.classList.contains('step_3')) {
const buyNowButton = document.querySelector('.buy-now-btn.complete-purchase');
if (buyNowButton && buyNowButton.style.display !== 'none') {
nextButton = buyNowButton;
}
}

console.log('Active step:', activeStep?.className);
console.log('Next button found:', nextButton);
if (nextButton) {
console.log('Next button position:', nextButton.getBoundingClientRect());
}

// Use the same fixed positioning for both desktop and mobile
speechBubble.style.position = 'fixed';
speechBubble.style.bottom = '100px';
speechBubble.style.right = '10px';
speechBubble.style.top = 'auto';
speechBubble.style.left = 'auto';
speechBubble.style.transform = 'none';
speechBubble.style.zIndex = '9999';

// Move bubble to body
if (speechBubble.parentElement !== document.body) {
document.body.appendChild(speechBubble);
}

speechBubble.textContent = message;
speechBubble.style.display = 'block';

// No need to scroll since bubble is in fixed position

// Hide after 3 seconds
setTimeout(() => {
speechBubble.style.display = 'none';
}, 3000);
}

// Update the populateTicketSections function to include order info
function populateTicketSections(eventData) {
if (!domCache.ticketSections) return;

// Update order info block
const orderInfoBlock = document.querySelector('.order-info-block');
if (orderInfoBlock) {
orderInfoBlock.innerHTML = `
Order Information

${eventData.event_date_string}

${eventData.venue_title}

${eventData.venue_full}

`;
}

const html = eventData.tickets.map(section => {
const sectionId = section.title.toLowerCase().replace(/s+/g, '_');

return `

${section.title}
${section.seating_type}

Ticket Type
Price Per Person
Number of Tickets*

${section.ticket_types.map(type => `

${type.label}

Online Special
€${type.price}

+

`).join('')}

`}).join('');

domCache.ticketSections.innerHTML = html;

// Add event listeners for quantity buttons
document.querySelectorAll('.ticket-quantity-btn').forEach(button => {
button.addEventListener('click', function() {
const section = this.dataset.section;
const ticketType = this.dataset.ticketType;
const price = parseFloat(this.dataset.price);
const quantityInput = document.querySelector(`.quantity-value[data-section="${section}"][data-ticket-type="${ticketType}"]`);
const currentValue = parseInt(quantityInput.value);

if (this.classList.contains('minus') && currentValue > 0) {
quantityInput.value = currentValue – 1;
updateTicketQuantity(section, ticketType, price, currentValue – 1);
} else if (this.classList.contains('plus')) {
const maxValue = parseInt(quantityInput.getAttribute('max'));
if (currentValue < maxValue) {
quantityInput.value = currentValue + 1;
updateTicketQuantity(section, ticketType, price, currentValue + 1);
}
}
});
});
}

function populateAddOns(eventData) {
if (!domCache.addOnsGrid) return;
// Build all HTML at once
const html = `

${eventData.add_ons.map(addon => `

${addon.label}
${addon.description ? `${addon.description}` : ''}
€${addon.price.toFixed(2)}

+

`).join('')}
`;
domCache.addOnsGrid.innerHTML = html;
// Add event listeners for quantity buttons
document.querySelectorAll('.addon-list-item .quantity-btn').forEach(button => {
button.addEventListener('click', function() {
const addonName = this.dataset.addonName;
const price = parseFloat(this.dataset.price);
const quantityInput = document.querySelector(`.addon-list-item .quantity-value[data-addon-name="${addonName}"]`);
const currentValue = parseInt(quantityInput.value);
if (this.classList.contains('minus') && currentValue > 0) {
quantityInput.value = currentValue – 1;
updateAddonQuantity(addonName, price, currentValue – 1);
} else if (this.classList.contains('plus')) {
const maxValue = parseInt(quantityInput.getAttribute('max'));
if (currentValue < maxValue) {
quantityInput.value = currentValue + 1;
updateAddonQuantity(addonName, price, currentValue + 1);
}
}
});
});
}

function checkAndHandleAddOns(eventData) {
// If add_ons is empty or all are disabled, hide step_2
const hasActiveAddOns = eventData.add_ons && eventData.add_ons.some(addon => addon.enabled !== false);
const step2 = document.querySelector('.step_2');
if (!hasActiveAddOns) {
if (step2) {
step2.classList.add('inactive');
step2.style.display = 'none';
}
window.skipAddOnsStep = true;
} else {
if (step2) {
step2.classList.remove('inactive');
step2.style.display = '';
}
window.skipAddOnsStep = false;
}
}

function updateAddonQuantity(addonName, price, quantity) {
console.log('updateAddonQuantity called with:', { addonName, price, quantity });
console.log('globalAddOnsData:', globalAddOnsData);

// Find or create addon in orderData
const addonIndex = orderData.addOns.findIndex(a => a.name === addonName);

if (addonIndex === -1 && quantity > 0) {
// Find the addon data to get the proper label
const addonData = globalAddOnsData.find(addon => addon.name === addonName);
const label = addonData?.label || addonName;

console.log('Found addon data:', addonData);
console.log('Using label:', label);

// Add new addon
orderData.addOns.push({
name: addonName,
label: label,
price: price,
quantity: quantity
});
} else if (addonIndex !== -1) {
if (quantity > 0) {
orderData.addOns[addonIndex].quantity = quantity;
} else {
// Remove addon if quantity is zero
orderData.addOns.splice(addonIndex, 1);
}
}

// Update cart display
updateOrderSummary();
saveOrderDataToStorage();
}

// Event delegation for ticket and add-on selects
function handleSelectChange(event) {
if (event.target.matches('select[name^="event_seat_"], .add-on-quantity select')) {
updateOrderSummary();
}
}

// Initialize event listeners using event delegation
function initializeEventListeners() {
if (domCache.form) {
domCache.form.addEventListener('change', handleSelectChange);
// Add form submission handler
domCache.form.addEventListener('submit', handleFormSubmit);
}

// Add click handler for complete purchase button
const completePurchaseBtn = document.querySelector('.next-step.complete-purchase');
if (completePurchaseBtn) {
completePurchaseBtn.addEventListener('click', function(event) {
event.preventDefault();
handleFormSubmit(event);
});
}

// Add voucher validation
const applyVoucherBtn = document.querySelector('.apply-voucher-btn');
const voucherInput = document.querySelector('input[name="voucher"]');
const warningVoucher = document.querySelector('.warning-voucher');
const successVoucher = document.querySelector('.success-voucher');

if (applyVoucherBtn && voucherInput) {
applyVoucherBtn.addEventListener('click', async function() {
const voucherCode = voucherInput.value.trim();

if (!voucherCode) {
warningVoucher.style.display = 'none';
successVoucher.style.display = 'none';
return;
}

try {
// Get event IDs from URL
const urlParams = new URLSearchParams(window.location.search);
const eventId = urlParams.get('event_id');
const eventDateId = urlParams.get('event_date_id');

if (!eventId) {
throw new Error('Missing event information');
}

// Call backend to validate voucher
const response = await fetch(`/api/validate-voucher.php?event_id=${eventId}&voucher_code=${encodeURIComponent(voucherCode)}`);
const result = await response.json();

if (result.valid) {
warningVoucher.style.display = 'none';
successVoucher.style.display = 'block';
successVoucher.textContent = `Voucher Applied: ${voucherCode} – Discount on tickets ${result.discount_type === 'percentage' ? '-' + result.discount + '%' : '-€' + result.discount}`;
// Update total with discount
updateOrderSummary();
} else {
warningVoucher.style.display = 'block';
successVoucher.style.display = 'none';
}
} catch (error) {
console.error('Error validating voucher:', error);
warningVoucher.style.display = 'block';
successVoucher.style.display = 'none';
}
});

// Clear messages when input changes
voucherInput.addEventListener('input', function() {
warningVoucher.style.display = 'none';
successVoucher.style.display = 'none';
});
}
}

// Main initialization
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Starting initialization…');
initializeDomCache();
initializeEventListeners();

// Initialize speech bubble warning
const speechBubble = document.querySelector('.speech-bubble-warning');
if (speechBubble) {
speechBubble.style.display = 'none';
}

// Add form submission handler
const form = document.getElementById('event-order-form');
if (form) {
form.addEventListener('submit', handleFormSubmit);
}

// Add click handler for complete purchase button
const completePurchaseBtn = document.querySelector('.next-step.complete-purchase');
if (completePurchaseBtn) {
completePurchaseBtn.addEventListener('click', function(event) {
event.preventDefault();
handleFormSubmit(event);
});
}

// Initialize step navigation
const params = getUrlParams();
if (params.event_id && params.event_date_id) {
fetchEventData(params.event_id, params.event_date_id)
.then(eventData => {
checkAndHandleAddOns(eventData);
populateTicketSections(eventData);
populateAddOns(eventData);
restoreSelectionsToUI(); // {
console.error('Error loading event data:', error);

// Check if it's a booking closed error
if (error.message && error.message.includes('Online Booking Closed')) {
// Get the event date from the URL parameters to calculate countdown
const params = getUrlParams();
const url = `/api/events.php?event_id=${params.event_id}&event_date_id=${params.event_date_id}`;

fetch(url)
.then(response => response.json())
.then(data => {
const eventDate = data.event_dates.find(date => date.event_date_id === parseInt(params.event_date_id));
if (eventDate) {
const showtime = new Date(eventDate.event_datetime);
const now = new Date();
const timeDiff = showtime – now;
const minutesUntilShow = Math.floor(timeDiff / (1000 * 60));

// Display online closed message with countdown
const ticketSections = document.getElementById('ticket-sections');
if (ticketSections) {
ticketSections.innerHTML = `

ONLINE CLOSED – Starts in ${minutesUntilShow} min

Go to Boxoffice

Box Office Information:
• Open 1 hour before showtime
• Cash and card payments accepted
• Subject to availability

`;
}
}
})
.catch(() => {
// Fallback if we can't get the countdown
const ticketSections = document.getElementById('ticket-sections');
if (ticketSections) {
ticketSections.innerHTML = `

ONLINE CLOSED

This show is starting within 30 minutes.

Go to Boxoffice

`;
}
});
} else {
// Show generic error message
showSpeechBubble('Error loading event data. Please refresh the page.');
}
});
}

// Wheelchair user logic (number input with +/-)
const wheelchairCountInput = document.getElementById('wheelchair-user-count');
const wheelchairMinus = document.getElementById('wheelchair-minus');
const wheelchairPlus = document.getElementById('wheelchair-plus');
if (wheelchairCountInput && wheelchairMinus && wheelchairPlus) {
wheelchairMinus.addEventListener('click', function() {
let val = parseInt(wheelchairCountInput.value) || 0;
if (val > 0) wheelchairCountInput.value = val – 1;
});
wheelchairPlus.addEventListener('click', function() {
let val = parseInt(wheelchairCountInput.value) || 0;
wheelchairCountInput.value = val + 1;
});
}
});

function getWheelchairUserData() {
const wheelchairCountInput = document.getElementById('wheelchair-user-count');
let wheelchairUsers = 0;
if (wheelchairCountInput) {
wheelchairUsers = parseInt(wheelchairCountInput.value) || 0;
}
return wheelchairUsers;
}

// In handleFormSubmit or wherever orderData is finalized before sending to backend:
// orderData.wheelchair_users = getWheelchairUserData();

// Cart functionality
let cartInitialized = false;
let isToggling = false;

function toggleCart(event) {
if (isToggling) return;
isToggling = true;

if (event) {
event.preventDefault();
event.stopPropagation();
}

console.log('Toggling cart', {
cartInitialized,
event: event ? event.type : 'manual',
target: event ? event.target : 'none'
});

const cartSlideOut = document.querySelector('.cart-slide-out');
const cartToggle = document.querySelector('.cart-toggle');
const cartToggleIcon = document.querySelector('.cart-toggle-icon');

if (!cartSlideOut || !cartToggle) {
console.error('Cart elements not found:', { cartSlideOut, cartToggle });
isToggling = false;
return;
}

const isActive = cartSlideOut.classList.contains('active');

if (isActive) {
cartSlideOut.classList.remove('active');
cartToggle.classList.remove('active');
if (cartToggleIcon) cartToggleIcon.classList.remove('active');
} else {
cartSlideOut.classList.add('active');
cartToggle.classList.add('active');
if (cartToggleIcon) cartToggleIcon.classList.add('active');
}

setTimeout(() => {
isToggling = false;
}, 100);
}

function initializeCart() {
if (cartInitialized) {
console.log('Cart already initialized');
return;
}

console.log('Initializing cart functionality');

// Remove any existing event listeners
const cartToggle = document.querySelector('.cart-toggle');
const innerCartToggle = document.querySelector('.cart-toggle.inner');

if (cartToggle) {
cartToggle.removeEventListener('click', toggleCart);
cartToggle.addEventListener('click', toggleCart);
}

if (innerCartToggle) {
innerCartToggle.removeEventListener('click', toggleCart);
innerCartToggle.addEventListener('click', toggleCart);
}

// Add click outside handler
document.removeEventListener('click', handleOutsideClick);
document.addEventListener('click', handleOutsideClick);

cartInitialized = true;
console.log('Cart initialization complete');
}

function handleOutsideClick(event) {
const cartSlideOut = document.querySelector('.cart-slide-out');
const cartToggle = document.querySelector('.cart-toggle');

if (cartSlideOut && cartSlideOut.classList.contains('active')) {
if (!cartSlideOut.contains(event.target) && !cartToggle.contains(event.target)) {
toggleCart();
}
}
}

// Initialize cart when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Initializing cart…');
initializeCart();
});

// Step Navigation
function goToStep(stepNumber) {
console.log('Navigating to step:', stepNumber);

// Validate step number
if (stepNumber < 1 || stepNumber > 3) {
console.warn('Invalid step number:', stepNumber);
return;
}

// If skipping step 2
if (window.skipAddOnsStep) {
if (stepNumber === 2) stepNumber = 3;
if (stepNumber === 0) stepNumber = 1;
}

// Validate ticket selection when moving to step 2 or 3
if (stepNumber === 2 || stepNumber === 3) {
let totalTickets = 0;
const quantityInputs = document.querySelectorAll('.quantity-value');

quantityInputs.forEach(input => {
totalTickets += parseInt(input.value) || 0;
});

if (totalTickets === 0) {
showSpeechBubble('Please select at least one ticket before proceeding');
return;
}
}

// Hide all steps first
const steps = document.querySelectorAll('.step_1, .step_2, .step_3');
steps.forEach(step => {
step.classList.remove('active');
console.log('Hiding step:', step.className);
});

// Show selected step
const selectedStep = document.querySelector(`.step_${stepNumber}`);
if (selectedStep) {
// Force a reflow to ensure the transition works
selectedStep.offsetHeight;
selectedStep.classList.add('active');
console.log('Showing step:', selectedStep.className);

// Initialize Stripe when reaching Step 3
if (stepNumber === 3) {
console.log('Reached Step 3, initializing Stripe…');
initializeStripe();
}
} else {
console.error(`Step ${stepNumber} element not found`);
return;
}

// Update step indicator
const stepIndicators = document.querySelectorAll('.step-indicator');
stepIndicators.forEach(indicator => {
indicator.textContent = `Step ${stepNumber} of 3`;
});

// Update navigation buttons
updateNavigationButtons(stepNumber);

// Scroll to top of the form
const form = document.getElementById('event-order-form');
if (form) {
form.scrollIntoView({ behavior: 'smooth' });
}
}

function updateNavigationButtons(currentStep) {
console.log('Updating navigation buttons for step:', currentStep);

// Update sticky navigation
const stickyPrev = document.querySelector('.sticky-cart-bar .prev-step');
const stickyNext = document.querySelector('.sticky-cart-bar .next-step');
const buyNowBtn = document.querySelector('.buy-now-btn');

if (stickyPrev) {
stickyPrev.style.display = 'block';
if (currentStep === 1) {
stickyPrev.innerHTML = '';
stickyPrev.onclick = function(event) {
event.preventDefault();
window.location.href = '/index.html';
};
} else {
stickyPrev.innerHTML = '';
stickyPrev.onclick = function(event) {
event.preventDefault();
goToStep(currentStep – 1);
};
}
}

if (stickyNext) {
if (currentStep === 3) {
// Hide the next button in sticky nav on step 3 but maintain space
stickyNext.style.visibility = 'hidden';
stickyNext.classList.add('hide-on-step3');
} else {
stickyNext.style.visibility = 'visible';
stickyNext.classList.remove('hide-on-step3');
stickyNext.innerHTML = '';
stickyNext.classList.remove('complete-purchase');
stickyNext.onclick = function(event) {
event.preventDefault();
goToStep(currentStep + 1);
};
}
}

// Show/hide buy now button
if (buyNowBtn) {
if (currentStep === 3) {
buyNowBtn.style.display = 'block';
buyNowBtn.onclick = function(event) {
event.preventDefault();
handleFormSubmit(event);
};
} else {
buyNowBtn.style.display = 'none';
}
}
}

// Initialize step navigation when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Initializing step navigation…');

// Ensure Step 1 is active by default
const step1 = document.querySelector('.step_1');
const step2 = document.querySelector('.step_2');
const step3 = document.querySelector('.step_3');

if (step1) {
step1.classList.add('active');
console.log('Initialized Step 1 as active');
}

if (step2) {
step2.classList.remove('active');
console.log('Initialized Step 2 as hidden');
}

if (step3) {
step3.classList.remove('active');
console.log('Initialized Step 3 as hidden');
}

// Add click handlers to step navigation buttons
const nextButtons = document.querySelectorAll('.next-step');
const prevButtons = document.querySelectorAll('.prev-step');

nextButtons.forEach(button => {
button.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation(); // Prevent event bubbling

// Find the current active step
const activeStep = document.querySelector('.step_1.active, .step_2.active, .step_3.active');
if (!activeStep) {
console.error('No active step found');
return;
}

const currentStepNumber = parseInt(activeStep.className.match(/step_(d+)/)?.[1] || '1');
console.log('Current step:', currentStepNumber);

// Validate ticket selection before proceeding
if (currentStepNumber === 1) {
let totalTickets = 0;
const quantityInputs = document.querySelectorAll('.quantity-value');

quantityInputs.forEach(input => {
totalTickets += parseInt(input.value) || 0;
});

if (totalTickets === 0) {
showSpeechBubble('Please select at least one ticket before proceeding');
return;
}
}

if (currentStepNumber < 3) {
// Add a small delay to prevent multiple transitions
setTimeout(() => {
goToStep(currentStepNumber + 1);
}, 100);
}
});
});

prevButtons.forEach(button => {
button.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation(); // Prevent event bubbling

// Find the current active step
const activeStep = document.querySelector('.step_1.active, .step_2.active, .step_3.active');
if (!activeStep) {
console.error('No active step found');
return;
}

const currentStepNumber = parseInt(activeStep.className.match(/step_(d+)/)?.[1] || '1');
console.log('Current step:', currentStepNumber);

if (currentStepNumber > 1) {
// If skipping step 2, go directly to step 1 from step 3
let targetStep = currentStepNumber – 1;
if (window.skipAddOnsStep && currentStepNumber === 3) {
targetStep = 1;
}
setTimeout(() => {
goToStep(targetStep);
}, 100);
}
});
});

// Initialize navigation buttons for the first step
updateNavigationButtons(1);
});

// Stripe Integration
let stripe;
let elements;
let stripeInitialized = false;

async function initializeStripe() {
if (stripeInitialized) {
console.log('Stripe already initialized');
return;
}

console.log('Initializing Stripe with Express Checkout…');
try {
// Initialize Stripe with your publishable key
stripe = Stripe('pk_live_KSh8MRi76Kliuk4fUpBZ23GS', {
apiVersion: '2023-10-16',
stripeAccount: 'acct_1C7mK9IQtQgABICR'
});

// Create elements with appearance configuration
elements = stripe.elements({
appearance: {
theme: 'stripe',
variables: {
colorPrimary: '#ed1e26',
colorBackground: '#ffffff',
colorText: '#30313d',
colorDanger: '#df1b41',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
spacingUnit: '4px',
borderRadius: '10px'
}
}
});

// Add custom CSS for larger payment buttons
const style = document.createElement('style');
style.textContent = `
.GooglePayButton {
border: 0 !important;
cursor: pointer !important;
max-height: 84px !important;
min-height: 52px !important;
position: relative !important;
}

.ApplePayButton {
border: 0 !important;
cursor: pointer !important;
max-height: 84px !important;
min-height: 52px !important;
position: relative !important;
}

#payment-request-button {
max-height: 84px !important;
min-height: 52px !important;
}

#payment-request-button button {
max-height: 84px !important;
min-height: 52px !important;
transform: scale(1.2) !important;
transform-origin: center !important;
}
`;
document.head.appendChild(style);

// Create and mount card elements with improved styling
const cardNumber = elements.create('cardNumber', {
placeholder: 'Card number',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card number element is properly mounted
const cardNumberContainer = document.getElementById('card-number');
if (cardNumberContainer) {
// Clear any existing content
cardNumberContainer.innerHTML = '';
cardNumber.mount('#card-number');
console.log('Card number element mounted successfully');
} else {
console.error('Card number container not found');
}

const cardExpiry = elements.create('cardExpiry', {
placeholder: 'MM/YY',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card expiry element is properly mounted
const cardExpiryContainer = document.getElementById('card-expiry');
if (cardExpiryContainer) {
// Clear any existing content
cardExpiryContainer.innerHTML = '';
cardExpiry.mount('#card-expiry');
console.log('Card expiry element mounted successfully');
} else {
console.error('Card expiry container not found');
}

const cardCvc = elements.create('cardCvc', {
placeholder: 'CVC',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card CVC element is properly mounted
const cardCvcContainer = document.getElementById('card-cvc');
if (cardCvcContainer) {
// Clear any existing content
cardCvcContainer.innerHTML = '';
cardCvc.mount('#card-cvc');
console.log('Card CVC element mounted successfully');
} else {
console.error('Card CVC container not found');
}

// Add event listeners to card elements for debugging
cardNumber.on('ready', () => {
console.log('Card number element ready');
// Fix accessibility warnings for hidden Stripe inputs
fixStripeAccessibilityWarnings();
});

cardNumber.on('focus', () => {
console.log('Card number focused');
});

cardNumber.on('blur', () => {
console.log('Card number blurred');
});

// Function to fix Stripe accessibility warnings
function fixStripeAccessibilityWarnings() {
// Remove aria-hidden from Stripe's hidden inputs that are focused
const hiddenInputs = document.querySelectorAll('.__PrivateStripeElement-input[aria-hidden="true"]');
hiddenInputs.forEach(input => {
// Remove aria-hidden attribute to prevent accessibility warnings
input.removeAttribute('aria-hidden');
// Ensure they're properly hidden with CSS instead
input.style.position = 'absolute';
input.style.left = '-9999px';
input.style.top = '-9999px';
input.style.width = '1px';
input.style.height = '1px';
input.style.opacity = '0';
input.style.pointerEvents = 'none';
// Prevent focus
input.setAttribute('tabindex', '-1');
input.style.pointerEvents = 'none';
});
}

// Set up MutationObserver to catch dynamically created Stripe elements
function setupStripeAccessibilityObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if the added node is a Stripe hidden input
if (node.classList && node.classList.contains('__PrivateStripeElement-input')) {
if (node.getAttribute('aria-hidden') === 'true') {
node.removeAttribute('aria-hidden');
node.style.position = 'absolute';
node.style.left = '-9999px';
node.style.top = '-9999px';
node.style.width = '1px';
node.style.height = '1px';
node.style.opacity = '0';
node.style.pointerEvents = 'none';
}
}
// Also check child elements
const hiddenInputs = node.querySelectorAll && node.querySelectorAll('.__PrivateStripeElement-input[aria-hidden="true"]');
if (hiddenInputs) {
hiddenInputs.forEach(input => {
input.removeAttribute('aria-hidden');
input.style.position = 'absolute';
input.style.left = '-9999px';
input.style.top = '-9999px';
input.style.width = '1px';
input.style.height = '1px';
input.style.opacity = '0';
input.style.pointerEvents = 'none';
});
}
}
});
}
});
});

// Start observing the document body for changes
observer.observe(document.body, {
childList: true,
subtree: true
});

return observer;
}

// — Stripe Express Checkout Element (New Implementation) —
// Calculate the total for Express Checkout
function getTotalAmountForExpressCheckout() {
let subtotal = 0;
orderData.tickets.forEach(ticket => {
if (ticket.quantity > 0) {
subtotal += ticket.quantity * ticket.price;
}
});
let addOnsTotal = 0;
orderData.addOns.forEach(addon => {
addOnsTotal += addon.quantity * addon.price;
});
let totalAmount = subtotal + addOnsTotal;

// Apply voucher discount if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (discountMatch) {
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];
if (discountType === '%') {
totalAmount = subtotal * (1 – discountValue / 100) + addOnsTotal;
} else {
totalAmount = Math.max(0, subtotal – discountValue) + addOnsTotal;
}
}
}
return Math.round(totalAmount * 100); // in cents
}

// Create Payment Request Button (legacy API – works without clientSecret)
const paymentRequestAmount = getTotalAmountForExpressCheckout();
console.log('Creating Payment Request Button with amount:', paymentRequestAmount);

// Create payment request
const paymentRequest = stripe.paymentRequest({
country: 'IE',
currency: 'eur',
total: {
label: 'Fossetts Circus Tickets',
amount: paymentRequestAmount,
},
requestPayerName: true,
requestPayerEmail: true,
requestPayerPhone: true,
});

// Create payment request button
let paymentRequestButton;
try {
console.log('Creating payment request button…');
paymentRequestButton = elements.create('paymentRequestButton', {
paymentRequest: paymentRequest,
style: {
paymentRequestButton: {
type: 'default',
theme: 'dark',
height: '44px'
}
}
});
console.log('Payment Request Button created successfully:', paymentRequestButton);
} catch (error) {
console.error('Error creating Payment Request Button:', error);
console.error('Error details:', {
message: error.message,
stack: error.stack,
name: error.name
});
return;
}

// Check if payment request is supported
paymentRequest.canMakePayment().then(function(result) {
console.log('Payment Request canMakePayment result:', result);
// TEMPORARY: Always hide Google Pay and Apple Pay button for debugging
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
return; // Prevent any further logic from showing the button
// — original logic below (commented out) —
/*
if (result) {
console.log('Payment Request is supported, mounting button…');
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer && paymentRequestAmount > 0) {
paymentRequestContainer.innerHTML = '';
paymentRequestContainer.style.display = 'block';
try {
paymentRequestButton.mount('#payment-request-button');
console.log('Payment Request Button mounted successfully');
} catch (error) {
console.error('Error mounting Payment Request Button:', error);
}
} else {
console.log('Payment Request container not found or amount is 0');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
}
} else {
console.log('Payment Request is not supported');
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
}
*/
});

// Handle payment request events
paymentRequest.on('paymentmethod', async function(ev) {
console.log('Payment Request paymentmethod event:', ev);

try {
// Prepare form data
const form = document.getElementById('event-order-form');
const formData = new FormData(form);

// Add wheelchair user count
const wheelchairUsers = getWheelchairUserData();
if (wheelchairUsers > 0) {
formData.append('wheelchair_users', wheelchairUsers);
}

// Add payment token (backend expects payment_token)
formData.append('payment_token', ev.paymentMethod.id);

// Add event and date ids from URL parameters
const urlParams = getUrlParams();
const eventId = urlParams.event_id;
const eventDateId = urlParams.event_date_id;

// Add event and date ids to form data
formData.append('event_id', eventId);
formData.append('event_date_id', eventDateId);

// Add customer information from Payment Request
formData.append('customer_name', ev.payerName || '');
formData.append('customer_email', ev.payerEmail || '');
formData.append('customer_confirm_email', ev.payerEmail || '');
formData.append('customer_phone', ev.payerPhone || '');

// Collect ticket quantities (same as regular form submission)
console.log('Collecting ticket quantities for Payment Request…');
const sectionQuantities = {};
const quantityInputs = document.querySelectorAll('.quantity-value[data-section][data-ticket-type]');

quantityInputs.forEach(input => {
const section = input.dataset.section;
const ticketType = input.dataset.ticketType;
const quantity = parseInt(input.value) || 0;

if (!sectionQuantities[section]) {
sectionQuantities[section] = {};
}

if (ticketType && quantity > 0) {
sectionQuantities[section][ticketType.toLowerCase()] = quantity;
}

console.log('Processing ticket input:', {
section,
ticketType,
quantity,
value: input.value
});
});

console.log('Section quantities:', sectionQuantities);

// Send all ticket types dynamically with seating area information
Object.entries(sectionQuantities).forEach(([section, sectionData]) => {
Object.entries(sectionData).forEach(([ticketType, quantity]) => {
// Convert spaces to underscores in the ticket type name for the form field
// The server expects: event_seat_{seating_area}_{ticket_type_with_underscores}
const cleanTicketType = ticketType.replace(/s+/g, '_');
formData.append(`event_seat_${section}_${cleanTicketType}`, quantity);

console.log('Added form field:', `event_seat_${section}_${cleanTicketType}`, '=', quantity);
console.log('Ticket type details:', {
section: section,
originalTicketType: ticketType,
cleanTicketType: cleanTicketType,
quantity: quantity,
fieldName: `event_seat_${section}_${cleanTicketType}`
});
});
});

// Add add-ons
console.log('Collecting add-ons for Payment Request…');
const addonInputs = document.querySelectorAll('.addon-list-item .quantity-value');
console.log('Found addon inputs:', addonInputs.length);

addonInputs.forEach(input => {
const addonId = input.dataset.addonId;
const addonName = input.dataset.addonName;
const quantity = parseInt(input.value) || 0;

console.log('Processing addon input:', {
addonId,
addonName,
quantity,
value: input.value
});

if (addonId && quantity > 0) {
formData.append(`addon_${addonId}`, quantity);
console.log(`Added addon_${addonId} = ${quantity}`);
}
});

// Add voucher if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const voucherMatch = voucherText.match(/Voucher: ([A-Z0-9]+)/);
if (voucherMatch) {
formData.append('voucher_code', voucherMatch[1]);
}
}

// Submit order to server
const response = await fetch(`/order.php?event_id=${eventId}&event_date_id=${eventDateId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});

const result = await response.json();
if (result.success) {
// Complete the payment request successfully
ev.complete('success');
window.location.href = result.redirect_url;
} else {
// Complete the payment request with error
ev.complete('fail');
showSpeechBubble(result.message || 'Payment failed. Please try again.');
}
} catch (error) {
console.error('Error handling Payment Request payment:', error);
// Complete the payment request with error
ev.complete('fail');
showSpeechBubble('Payment processing failed. Please try again.');
}
});

paymentRequest.on('cancel', function() {
console.log('Payment Request cancelled');
});

// Update Payment Request amount when order changes
function updatePaymentRequestAmount() {
const newAmount = getTotalAmountForExpressCheckout();
paymentRequest.update({
total: {
label: 'Fossetts Circus Tickets',
amount: newAmount,
}
});
}

// Store the update function globally for use in order updates
window.updatePaymentRequestAmount = updatePaymentRequestAmount;

stripeInitialized = true;
console.log('Stripe Payment Request Button initialized successfully');

// Set up observer to catch dynamically created Stripe elements
const stripeObserver = setupStripeAccessibilityObserver();

// Suppress specific Stripe accessibility warnings
const originalWarn = console.warn;
console.warn = function(…args) {
const message = args.join(' ');
if (message.includes('aria-hidden') && message.includes('Stripe')) {
// Suppress Stripe aria-hidden warnings
return;
}
originalWarn.apply(console, args);
};

// Fix accessibility warnings after all elements are mounted
setTimeout(() => {
fixStripeAccessibilityWarnings();
}, 1000);

} catch (error) {
console.error('Error initializing Stripe Express Checkout:', error);
throw error;
}
}

// Initialize payment functionality when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Initializing payment…');

// Initialize Stripe
initializeStripe();

// Add form submit handler
const form = document.getElementById('event-order-form');
if (form) {
form.addEventListener('submit', handleFormSubmit);
}

// Add click handler for complete purchase button
const completePurchaseBtn = document.querySelector('.next-step.complete-purchase');
if (completePurchaseBtn) {
completePurchaseBtn.addEventListener('click', function(event) {
event.preventDefault();
handleFormSubmit(event);
});
}
});

async function handleFormSubmit(event) {
event.preventDefault();
console.log('Form submission started');

// Ensure order summary is up to date before checking total
updateOrderSummary();

// Recalculate total directly before checking minimum
let subtotal = 0;
orderData.tickets.forEach(ticket => {
if (ticket.quantity > 0) {
subtotal += ticket.quantity * ticket.price;
}
});
let addOnsTotal = 0;
orderData.addOns.forEach(addon => {
addOnsTotal += addon.quantity * addon.price;
});
let totalAmount = subtotal + addOnsTotal;

// Apply voucher discount if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (discountMatch) {
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];
if (discountType === '%') {
totalAmount = subtotal * (1 – discountValue / 100) + addOnsTotal;
} else {
totalAmount = Math.max(0, subtotal – discountValue) + addOnsTotal;
}
}
}

// Log detailed payment calculation
console.log('Payment calculation details:', {
ticketSubtotal: subtotal,
addOnsTotal: addOnsTotal,
totalBeforeVoucher: subtotal + addOnsTotal,
totalAfterVoucher: totalAmount,
ticketDetails: orderData.tickets.map(t => ({
type: t.type,
quantity: t.quantity,
price: t.price,
subtotal: t.quantity * t.price
})),
addonDetails: orderData.addOns.map(a => ({
name: a.name,
quantity: a.quantity,
price: a.price,
subtotal: a.quantity * a.price
}))
});

// Validate minimum payment amount
if (totalAmount < 0.5) {
const errorMessage = `The minimum payment amount is €0.50. Current total: €${totalAmount.toFixed(2)}`;
console.error(errorMessage);
showSpeechBubble(errorMessage);
return;
}

// Validate form
const form = document.getElementById('event-order-form');
if (!form) {
console.error('Form not found');
return;
}

// Get form elements safely
const customerName = form.querySelector('[name="customer_name"]')?.value?.trim();
const customerPhone = form.querySelector('[name="customer_phone"]')?.value?.trim();
const email = document.getElementById('email_address')?.value?.trim();
const confirmEmail = document.getElementById('confirm_email_address')?.value?.trim();

// Validate required fields
if (!customerName) {
showSpeechBubble('Please enter your full name');
return;
}
if (!customerPhone) {
showSpeechBubble('Please enter your phone number');
return;
}
if (!email) {
showSpeechBubble('Please enter your email address');
return;
}
if (!confirmEmail) {
showSpeechBubble('Please confirm your email address');
return;
}

// Validate email match
if (email !== confirmEmail) {
showSpeechBubble('Email addresses do not match');
return;
}

// Validate email format
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
if (!emailRegex.test(email)) {
showSpeechBubble('Please enter a valid email address');
return;
}

// Validate phone number (basic validation)
const phoneRegex = /^[ds+()-]{8,}$/;
if (!phoneRegex.test(customerPhone)) {
showSpeechBubble('Please enter a valid phone number');
return;
}

// Validate card details
if (!stripe || !elements) {
showSpeechBubble('Payment system not initialized. Please refresh the page.');
return;
}

const cardNumber = elements.getElement('cardNumber');
const cardExpiry = elements.getElement('cardExpiry');
const cardCvc = elements.getElement('cardCvc');

if (!cardNumber || !cardExpiry || !cardCvc) {
showSpeechBubble('Please enter all card details');
return;
}

// Get submit button (check both locations)
const submitButton = document.querySelector('.buy-now-btn') || document.querySelector('.next-step.complete-purchase');
if (!submitButton) {
console.error('Submit button not found');
showSpeechBubble('Error: Submit button not found. Please refresh the page.');
return;
}

// Disable submit button
submitButton.disabled = true;
submitButton.textContent = 'Processing…';

try {
// Get event IDs from URL
const urlParams = new URLSearchParams(window.location.search);
const eventId = urlParams.get('event_id');
const eventDateId = urlParams.get('event_date_id');

if (!eventId || !eventDateId) {
throw new Error('Missing event information');
}

// Create FormData object
const formData = new FormData();

// Add form data
formData.append('customer_name', customerName);
formData.append('customer_email', email);
formData.append('customer_confirm_email', confirmEmail);
formData.append('customer_phone', customerPhone);
formData.append('event_id', eventId);
formData.append('event_date_id', eventDateId);

// Sum ticket quantities across all sections – DYNAMIC APPROACH
// Instead of hardcoded mappings, we'll send all ticket types as they are

// Debug log for ticket collection
console.log('Collecting ticket quantities…');

// Collect all quantities by section and type
const sectionQuantities = {};
const quantityInputs = document.querySelectorAll('.quantity-value[data-section][data-ticket-type]');

quantityInputs.forEach(input => {
const section = input.dataset.section;
const ticketType = input.dataset.ticketType;
const quantity = parseInt(input.value) || 0;

if (!sectionQuantities[section]) {
sectionQuantities[section] = {};
}

if (ticketType && quantity > 0) {
// Store the original ticket type name as it appears in the database
sectionQuantities[section][ticketType] = quantity;
}

console.log('Processing ticket input:', {
section,
ticketType,
quantity,
value: input.value
});
});

// Log the collected ticket counts
console.log('Section quantities:', sectionQuantities);

// Send all ticket types dynamically with seating area information
Object.entries(sectionQuantities).forEach(([section, sectionData]) => {
Object.entries(sectionData).forEach(([ticketType, quantity]) => {
// Convert spaces to underscores in the ticket type name for the form field
// The server expects: event_seat_{seating_area}_{ticket_type_with_underscores}
const cleanTicketType = ticketType.replace(/s+/g, '_');
formData.append(`event_seat_${section}_${cleanTicketType}`, quantity);

console.log('Added form field:', `event_seat_${section}_${cleanTicketType}`, '=', quantity);
console.log('Ticket type details:', {
section: section,
originalTicketType: ticketType,
cleanTicketType: cleanTicketType,
quantity: quantity,
fieldName: `event_seat_${section}_${cleanTicketType}`
});
});
});

// Add add-ons
console.log('Collecting add-ons…');
const addonInputs = document.querySelectorAll('.addon-list-item .quantity-value');
console.log('Found addon inputs:', addonInputs.length);

addonInputs.forEach(input => {
const addonId = input.dataset.addonId;
const addonName = input.dataset.addonName;
const quantity = parseInt(input.value) || 0;

console.log('Processing addon input:', {
addonId,
addonName,
quantity,
value: input.value,
dataset: input.dataset
});

if (addonId && quantity > 0) {
formData.append(`addon_${addonId}`, quantity);
console.log(`Added addon_${addonId} = ${quantity}`);
}
});

// Add voucher if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherInput = document.querySelector('input[name="voucher"]');
if (voucherInput) {
formData.append('voucher', voucherInput.value.trim());
}
}

// Add wheelchair user count
const wheelchairUsers = getWheelchairUserData();
if (wheelchairUsers > 0) {
formData.append('wheelchair_users', wheelchairUsers);
}

console.log('Submitting form data:', {
eventId,
eventDateId,
customerName,
email,
customerPhone,
formData: Object.fromEntries(formData)
});

// Debug: Log all form data entries
console.log('All form data entries:');
for (let [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}

// Create payment method using existing card elements
const { error: paymentMethodError, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardNumber,
billing_details: {
name: customerName,
email: email,
phone: customerPhone
}
});

if (paymentMethodError) {
throw new Error(paymentMethodError.message);
}

// Add payment token to form data
formData.append('payment_token', paymentMethod.id);

// Submit order to server
const response = await fetch(`/order.php?event_id=${eventId}&event_date_id=${eventDateId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});

console.log('Server response status:', response.status);
const responseText = await response.text();
console.log('Server response text:', responseText);

let result;
try {
result = JSON.parse(responseText);
console.log('Parsed server response:', result);

// Log detailed error information if present
if (!result.success) {
console.error('Server error details:', {
message: result.message,
status: result.status,
total: totalAmount,
formData: Object.fromEntries(formData)
});
}
} catch (e) {
console.error('Failed to parse server response:', e);
console.error('Raw response:', responseText);
throw new Error('Server returned an invalid response');
}

if (!result) {
throw new Error('Empty response from server');
}

if (result.error) {
throw new Error(result.error);
}

if (result.requires_action) {
// Handle 3D Secure authentication
const { error: confirmError } = await stripe.confirmCardPayment(
result.payment_intent_client_secret
);

if (confirmError) {
throw new Error(confirmError.message);
}

// Retry the order submission with the payment intent ID
formData.append('payment_intent_id', result.payment_intent_id);
const finalResponse = await fetch(`/order.php?event_id=${eventId}&event_date_id=${eventDateId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});

if (!finalResponse.ok) {
throw new Error(`Server error: ${finalResponse.status}`);
}

const finalResult = await finalResponse.json();
if (!finalResult.success) {
throw new Error(finalResult.message || 'Order processing failed');
}

// Redirect directly to the detailed success page
if (finalResult.redirect_url) {
window.location.href = finalResult.redirect_url;
} else {
// Fallback if no redirect URL
const successMessage = document.createElement('div');
successMessage.className = 'success-message';
successMessage.innerHTML = `
Order Successful!
Thank you for your purchase. Your order has been processed successfully.
Order Reference: ${finalResult.order_code || 'N/A'}
A confirmation email has been sent to ${email}

Return to Home

`;

// Replace form content with success message
const form = document.getElementById('event-order-form');
if (form) {
form.innerHTML = '';
form.appendChild(successMessage);
}
}
} else if (result.success) {
// Redirect directly to the detailed success page
if (result.redirect_url) {
window.location.href = result.redirect_url;
} else {
// Fallback if no redirect URL
const successMessage = document.createElement('div');
successMessage.className = 'success-message';
successMessage.innerHTML = `
Order Successful!
Thank you for your purchase. Your order has been processed successfully.
Order Reference: ${result.order_code || 'N/A'}
A confirmation email has been sent to ${email}

Return to Home

`;

// Replace form content with success message
const form = document.getElementById('event-order-form');
if (form) {
form.innerHTML = '';
form.appendChild(successMessage);
}
}
} else {
throw new Error(result.message || 'Order processing failed');
}

} catch (error) {
console.error('Payment error:', error);
showSpeechBubble(error.message || 'An error occurred during payment processing');
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = 'Complete Purchase';
}
}
}

function updateCartDisplay(ticketData, subtotal, addOnsTotal, total, shouldSave = true) {
console.log('updateCartDisplay called with:', { ticketData, subtotal, addOnsTotal, total, shouldSave });
console.log('orderData.addOns:', orderData.addOns);

// Generate cart items HTML
let cartItemsHTML = '';

// Add ticket items
if (Array.isArray(ticketData)) {
ticketData.forEach(ticket => {
// Format the section name (remove underscores and capitalize)
const sectionName = ticket.section
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');

// Dynamic ticket type mapping – handle any ticket type
let typeLabel = ticket.type;

// Only apply mapping for legacy ticket types, keep new types as-is
const legacyTypeMapping = {
'FULL': 'Adult',
'CHILD': 'Child',
'SENIOR': 'Senior'
};

if (legacyTypeMapping[ticket.type]) {
typeLabel = legacyTypeMapping[ticket.type];
}

cartItemsHTML += `

${typeLabel} – ${sectionName}
€${ticket.price.toFixed(2)} each remove

x${ticket.quantity}
€${(ticket.price * ticket.quantity).toFixed(2)}

`;
});
}

// Add add-ons
if (Array.isArray(orderData.addOns)) {
console.log('Processing add-ons in updateCartDisplay:', orderData.addOns);
orderData.addOns.forEach(addon => {
if (addon.quantity > 0) {
const displayName = addon.label || addon.name;
console.log('Addon display name:', displayName, 'for addon:', addon);
cartItemsHTML += `

${displayName}
€${addon.price.toFixed(2)} each remove

x${addon.quantity}
€${(addon.price * addon.quantity).toFixed(2)}

`;
}
});
}

// Add voucher discount if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const voucherMatch = voucherText.match(/Voucher Applied: (w+)/);
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (voucherMatch && discountMatch) {
const voucherCode = voucherMatch[1];
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];
let discountAmount = 0;

if (discountType === '%') {
// Apply discount only to ticket subtotal
discountAmount = subtotal * (discountValue / 100);
} else {
// For fixed amount, apply to ticket total only
discountAmount = Math.min(discountValue, subtotal);
}

cartItemsHTML += `

Voucher Applied: ${voucherCode}
Discount on tickets -${discountValue}${discountType} (remove)

-€${discountAmount.toFixed(2)}

`;
}
}

// Update all cart items containers
const cartItemsContainers = document.querySelectorAll('.cart-items');
cartItemsContainers.forEach(container => {
if (container) {
container.innerHTML = cartItemsHTML;
}
});

// Update the main order summary total
const summaryTotal = document.getElementById('summary-total-amount');
if (summaryTotal) {
summaryTotal.textContent = `€${total.toFixed(2)}`;
}

// Update the sticky cart bar total
const stickyCartTotal = document.querySelector('.sticky-cart-bar .cart-total-amount');
if (stickyCartTotal) {
stickyCartTotal.textContent = `€${total.toFixed(2)}`;
}

// Add click handlers for remove buttons
document.querySelectorAll('.remove-item').forEach(button => {
button.addEventListener('click', function() {
const type = this.dataset.type;
console.log('Remove button clicked:', { type, dataset: this.dataset });

if (type === 'ticket') {
const section = this.dataset.section;
const ticketType = this.dataset.ticketType;

console.log('Attempting to remove ticket:', { section, ticketType });

// Try to find the quantity input with the exact ticket type
let quantityInput = document.querySelector(`.quantity-value[data-section="${section}"][data-ticket-type="${ticketType}"]`);

// If not found, try case-insensitive search
if (!quantityInput) {
const allQuantityInputs = document.querySelectorAll('.quantity-value[data-section="' + section + '"]');
console.log('All quantity inputs for section', section, ':', allQuantityInputs);

// Try to find by exact match first, then case-insensitive
for (let input of allQuantityInputs) {
const inputTicketType = input.dataset.ticketType;
if (inputTicketType === ticketType ||
(inputTicketType && inputTicketType.toLowerCase() === ticketType.toLowerCase())) {
quantityInput = input;
console.log('Found quantity input:', inputTicketType);
break;
}
}
}

if (quantityInput) {
console.log('Found quantity input, setting to 0:', quantityInput);
quantityInput.value = '0';
updateTicketQuantity(section, ticketType, parseFloat(quantityInput.dataset.price), 0);
} else {
console.error('Could not find quantity input for ticket removal:', { section, ticketType });

// Fallback: manually remove from orderData
const ticketIndex = orderData.tickets.findIndex(t =>
t.section === section &&
(t.type === ticketType || t.type.toLowerCase() === ticketType.toLowerCase())
);

if (ticketIndex !== -1) {
console.log('Removing ticket from orderData directly:', orderData.tickets[ticketIndex]);
orderData.tickets.splice(ticketIndex, 1);
updateOrderSummary();
} else {
console.error('Could not find ticket in orderData to remove');
// Force refresh the cart display
updateOrderSummary();
}
}
} else if (type === 'addon') {
const name = this.dataset.name;
const quantityInput = document.querySelector(`.add-on-item .quantity-value[data-addon-name="${name}"]`);
if (quantityInput) {
quantityInput.value = '0';
updateAddonQuantity(name, parseFloat(quantityInput.dataset.price), 0);
}
} else if (type === 'voucher') {
// Clear voucher
const voucherInput = document.querySelector('input[name="voucher"]');
const warningVoucher = document.querySelector('.warning-voucher');
const successVoucher = document.querySelector('.success-voucher');
if (voucherInput) voucherInput.value = '';
if (warningVoucher) warningVoucher.style.display = 'none';
if (successVoucher) successVoucher.style.display = 'none';
updateOrderSummary();
}
});
});

// Only save order data if shouldSave is true
if (shouldSave) {
saveOrderData({
tickets: ticketData || [],
addOns: orderData.addOns || [],
total: total
});
}
}

function addRemoveListenersToSummary() {
document.querySelectorAll('.remove-item').forEach(el => {
el.addEventListener('click', function() {
const type = this.getAttribute('data-type');
const name = this.getAttribute('data-name');
if (type === 'addon') {
updateAddonQuantity(name, 0, 0);
// Update the UI input if present
const input = document.querySelector(`.quantity-value[data-addon-name="${name}"]`);
if (input) input.value = 0;
} else if (type === 'ticket') {
const section = this.getAttribute('data-section');
const ticketType = this.getAttribute('data-ticket-type');
const input = document.querySelector(`.quantity-value[data-section="${section}"][data-ticket-type="${ticketType.toLowerCase()}"]`);
if (input) {
input.value = 0;
input.dispatchEvent(new Event('change', { bubbles: true }));
}
}
});
});
}

function getUrlParams() {
const params = new URLSearchParams(window.location.search);
return {
event_id: params.get('event_id'),
event_date_id: params.get('event_date_id')
};
}

async function fetchEventData(event_id, event_date_id) {
console.log('Starting fetchEventData…');
console.log('URL parameters:', { event_id, event_date_id });

try {
const url = `/api/events.php?event_id=${event_id}&event_date_id=${event_date_id}`;
console.log('Making request to:', url);

const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
cache: 'no-cache'
});

console.log('Response status:', response.status);

if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}

const data = await response.json();
console.log('API response:', data);

if (data.error) {
throw new Error(data.error);
}

// Find the specific event date
const eventDate = data.event_dates.find(date => date.event_date_id === parseInt(event_date_id));
console.log('Found event date:', eventDate);

if (!eventDate) {
throw new Error('Event date not found');
}

// Check if showtime is active
if (eventDate.showtime_status !== 'active') {
throw new Error('This showtime is not available for booking');
}

// Check if booking is closed (within 30 minutes of showtime)
if (eventDate.booking_closed) {
// Calculate minutes until showtime
const showtime = new Date(eventDate.event_datetime);
const now = new Date();
const timeDiff = showtime – now;
const minutesUntilShow = Math.floor(timeDiff / (1000 * 60));

// Show online closed message with countdown
const onlineClosedMessage = `ONLINE CLOSED – Starts in ${minutesUntilShow} min`;

// Update the page to show online closed message
document.body.innerHTML = `

${onlineClosedMessage}

Go to Boxoffice

Return to Home

`;
return; // Stop further processing
}

// Format the date string
const dateString = new Date(eventDate.event_datetime).toLocaleString('en-GB', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
console.log('Formatted date string:', dateString);

// Store event date string globally
selectedEventDateString = dateString;

// Update venue info in DOM
const venueTitle = document.getElementById('venue-title');
const venueFull = document.getElementById('venue-full');
const eventDateElement = document.getElementById('event-date');
const summaryEventDate = document.getElementById('summary-event-date');

if (venueTitle) venueTitle.textContent = data.event.venue_title;
if (venueFull) venueFull.textContent = data.event.venue_full;
if (eventDateElement) eventDateElement.textContent = dateString;
if (summaryEventDate) summaryEventDate.textContent = dateString;

// Update order info block
const orderInfoBlock = document.querySelector('.order-info-block');
if (orderInfoBlock) {
orderInfoBlock.innerHTML = `
Order Information

${dateString}

${data.event.venue_title}

${data.event.venue_full}

`;
}

// Fetch dynamic add-ons
let addOns = [];
try {
const addonsResponse = await fetch('https://boxoffice.fossettscircus.com/api/addons', {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
cache: 'no-cache'
});

if (addonsResponse.ok) {
const contentType = addonsResponse.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const addonsData = await addonsResponse.json();
addOns = addonsData.map(addon => ({
id: addon.id,
name: addon.name.toLowerCase().replace(/s+/g, ''),
label: addon.name,
description: addon.description || '',
price: parseFloat(addon.price),
img: addon.thumb || '/elements/fossetts-popcorn.jpg'
}));
}
}
} catch (e) {
console.warn('Error fetching add-ons:', e);
}

// Store add-ons data globally for label lookup
globalAddOnsData = addOns;
console.log('Global add-ons data stored:', globalAddOnsData);

// Process the new data structure with capacities and ticket_definitions
const tickets = [];

// Group ticket definitions by seating area
const ticketGroups = {};
eventDate.ticket_definitions.forEach(ticketDef => {
if (!ticketGroups[ticketDef.seating_area]) {
ticketGroups[ticketDef.seating_area] = [];
}
ticketGroups[ticketDef.seating_area].push(ticketDef);
});

// Create ticket sections for each seating area
Object.keys(ticketGroups).forEach(seatingArea => {
const ticketDefs = ticketGroups[seatingArea];
const capacity = eventDate.capacities.find(cap => cap.seating_area === seatingArea);

console.log('Processing seating area:', seatingArea);
console.log('Ticket definitions for this area:', ticketDefs);

tickets.push({
title: seatingArea,
css_class: seatingArea.toLowerCase().replace(/s+/g, '-'),
seating_type: 'Non allocated seating',
capacity: capacity ? capacity.capacity : 0,
ticket_types: ticketDefs.map(ticketDef => {
console.log('Processing ticket definition:', ticketDef);
return {
label: ticketDef.name,
price: parseFloat(ticketDef.price),
select_name: `event_seat_${ticketDef.name.toLowerCase().replace(/s+/g, '_')}`,
seats_per_ticket: ticketDef.seats_per_ticket || 1
};
})
});
});

console.log('Processed tickets:', tickets);

return {
venue_title: data.event.venue_title || '',
venue_full: data.event.venue_full || '',
event_date_string: dateString,
tickets: tickets,
add_ons: addOns
};
} catch (error) {
console.error('Error in fetchEventData:', error);
throw error;
}
}

function createTicketType(ticket) {
const ticketType = document.createElement('div');
ticketType.className = 'ticket-type';
ticketType.innerHTML = `
${ticket.ticket_type}


0
+

`;

// Add event listeners for plus and minus buttons
const minusBtn = ticketType.querySelector('.minus');
const plusBtn = ticketType.querySelector('.plus');
const quantityValue = ticketType.querySelector('.quantity-value');

minusBtn.addEventListener('click', () => {
const currentValue = parseInt(quantityValue.textContent);
if (currentValue > 0) {
quantityValue.textContent = currentValue – 1;
updateTicketQuantity(ticket.ticket_id, currentValue – 1);
}
});

plusBtn.addEventListener('click', () => {
const currentValue = parseInt(quantityValue.textContent);
if (currentValue < ticket.seats_left) {
quantityValue.textContent = currentValue + 1;
updateTicketQuantity(ticket.ticket_id, currentValue + 1);
}
});

return ticketType;
}

function updateTicketQuantity(section, ticketType, price, quantity) {
// Normalize ticket type to uppercase for consistency, but preserve original for display
const normalizedTicketType = ticketType.toUpperCase();

// Find or create ticket in orderData
const ticketIndex = orderData.tickets.findIndex(t =>
t.section === section && t.type === normalizedTicketType
);

if (ticketIndex === -1) {
// Add new ticket
orderData.tickets.push({
section: section,
type: normalizedTicketType,
price: price,
quantity: quantity
});
} else {
// Update existing ticket
orderData.tickets[ticketIndex].quantity = quantity;
}

console.log('Updated ticket quantity:', {
section: section,
ticketType: ticketType,
normalizedType: normalizedTicketType,
price: price,
quantity: quantity,
orderData: orderData.tickets
});

// Update cart display
updateOrderSummary();
}

Assistant
User is using Stripe Checkout with Elements integration and a custom-built frontend, facing an issue where visitors coming from Facebook cannot interact with Stripe credit card fields.
Assistant
The issue you're facing — where Stripe credit card fields are not usable when users come from Facebook — is likely caused by **third-party cookie or iframe restrictions within Facebook's in-app browser**.

### 🔍 Why This Happens
When a user clicks a link in Facebook (or Instagram), it opens the page in a **webview inside the Facebook app**, not a regular browser. These embedded browsers:
– Restrict third-party scripts (like Stripe’s `iframe`-based Elements).
– Block or sandbox inputs, causing the credit card fields to become unresponsive or non-interactive.

### ✅ How to Detect and Solve It

#### 1. **Detect if User is Inside Facebook App**
Add this check in your JS:

function isFacebookInAppBrowser() {
  return /FBAN|FBAV|Instagram/.test(navigator.userAgent);
}

if (isFacebookInAppBrowser()) {
  alert("For best experience, please open this site in your regular browser (Chrome, Safari, etc.)");
}

You could also display a full-screen overlay with a message and an "Open in Browser" button.

#### 2. **Open in External Browser (Optional Prompt)**
Unfortunately, **JavaScript cannot force open the external browser** from inside Facebook, but you can:
– Add a CTA: “⚠️ Please open in Safari or Chrome to complete payment securely.”
– Provide a deep link using `target="_blank"` (sometimes helps open externally, but not always consistent).

#### 3. **Use Stripe's Hosted Checkout (if possible)**
Stripe's [Hosted Checkout](https://stripe.com/docs/checkout) page **works around these issues** since it’s hosted on Stripe’s own domain and has proper CSP and browser support.

If you can switch, this is the most robust fix.

#### 4. **Alternative Fix: Payment Link**
If you're not ready to move to hosted checkout, you can dynamically redirect users from in-app browsers to a Stripe [Payment Link](https://stripe.com/docs/payments/checkout/payment-links) instead of embedding Elements.

### ✅ Summary

| Approach | Description | Effort |
|—————————–|————————————————|———|
| **Use Stripe Hosted Checkout** | Most robust fix, works everywhere | ⭐⭐⭐ |
| **Detect in-app browser + alert** | Immediate fix, shows users a message | ⭐ |
| **Offer Payment Link** | Quick workaround if switching integration | ⭐⭐ |

Would you like a compact script to automatically show a "Open in Browser" overlay with a better UI?

User
// Stripe Integration
let stripe;
let elements;
let stripeInitialized = false;

async function initializeStripe() {
if (stripeInitialized) {
console.log('Stripe already initialized');
return;
}

console.log('Initializing Stripe with Express Checkout…');
try {
// Initialize Stripe with your publishable key
stripe = Stripe('pk_live_KSh8MRi76Kliuk4fUpBZ23GS', {
apiVersion: '2023-10-16',
stripeAccount: 'acct_1C7mK9IQtQgABICR'
});

// Create elements with appearance configuration
elements = stripe.elements({
appearance: {
theme: 'stripe',
variables: {
colorPrimary: '#ed1e26',
colorBackground: '#ffffff',
colorText: '#30313d',
colorDanger: '#df1b41',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
spacingUnit: '4px',
borderRadius: '10px'
}
}
});

// Add custom CSS for larger payment buttons
const style = document.createElement('style');
style.textContent = `
.GooglePayButton {
border: 0 !important;
cursor: pointer !important;
max-height: 84px !important;
min-height: 52px !important;
position: relative !important;
}

.ApplePayButton {
border: 0 !important;
cursor: pointer !important;
max-height: 84px !important;
min-height: 52px !important;
position: relative !important;
}

#payment-request-button {
max-height: 84px !important;
min-height: 52px !important;
}

#payment-request-button button {
max-height: 84px !important;
min-height: 52px !important;
transform: scale(1.2) !important;
transform-origin: center !important;
}
`;
document.head.appendChild(style);

// Create and mount card elements with improved styling
const cardNumber = elements.create('cardNumber', {
placeholder: 'Card number',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card number element is properly mounted
const cardNumberContainer = document.getElementById('card-number');
if (cardNumberContainer) {
// Clear any existing content
cardNumberContainer.innerHTML = '';
cardNumber.mount('#card-number');
console.log('Card number element mounted successfully');
} else {
console.error('Card number container not found');
}

const cardExpiry = elements.create('cardExpiry', {
placeholder: 'MM/YY',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card expiry element is properly mounted
const cardExpiryContainer = document.getElementById('card-expiry');
if (cardExpiryContainer) {
// Clear any existing content
cardExpiryContainer.innerHTML = '';
cardExpiry.mount('#card-expiry');
console.log('Card expiry element mounted successfully');
} else {
console.error('Card expiry container not found');
}

const cardCvc = elements.create('cardCvc', {
placeholder: 'CVC',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card CVC element is properly mounted
const cardCvcContainer = document.getElementById('card-cvc');
if (cardCvcContainer) {
// Clear any existing content
cardCvcContainer.innerHTML = '';
cardCvc.mount('#card-cvc');
console.log('Card CVC element mounted successfully');
} else {
console.error('Card CVC container not found');
}

// Add event listeners to card elements for debugging
cardNumber.on('ready', () => {
console.log('Card number element ready');
// Fix accessibility warnings for hidden Stripe inputs
fixStripeAccessibilityWarnings();
});

cardNumber.on('focus', () => {
console.log('Card number focused');
});

cardNumber.on('blur', () => {
console.log('Card number blurred');
});

// Function to fix Stripe accessibility warnings
function fixStripeAccessibilityWarnings() {
// Remove aria-hidden from Stripe's hidden inputs that are focused
const hiddenInputs = document.querySelectorAll('.__PrivateStripeElement-input[aria-hidden="true"]');
hiddenInputs.forEach(input => {
// Remove aria-hidden attribute to prevent accessibility warnings
input.removeAttribute('aria-hidden');
// Ensure they're properly hidden with CSS instead
input.style.position = 'absolute';
input.style.left = '-9999px';
input.style.top = '-9999px';
input.style.width = '1px';
input.style.height = '1px';
input.style.opacity = '0';
input.style.pointerEvents = 'none';
// Prevent focus
input.setAttribute('tabindex', '-1');
input.style.pointerEvents = 'none';
});
}

// Set up MutationObserver to catch dynamically created Stripe elements
function setupStripeAccessibilityObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if the added node is a Stripe hidden input
if (node.classList && node.classList.contains('__PrivateStripeElement-input')) {
if (node.getAttribute('aria-hidden') === 'true') {
node.removeAttribute('aria-hidden');
node.style.position = 'absolute';
node.style.left = '-9999px';
node.style.top = '-9999px';
node.style.width = '1px';
node.style.height = '1px';
node.style.opacity = '0';
node.style.pointerEvents = 'none';
}
}
// Also check child elements
const hiddenInputs = node.querySelectorAll && node.querySelectorAll('.__PrivateStripeElement-input[aria-hidden="true"]');
if (hiddenInputs) {
hiddenInputs.forEach(input => {
input.removeAttribute('aria-hidden');
input.style.position = 'absolute';
input.style.left = '-9999px';
input.style.top = '-9999px';
input.style.width = '1px';
input.style.height = '1px';
input.style.opacity = '0';
input.style.pointerEvents = 'none';
});
}
}
});
}
});
});

// Start observing the document body for changes
observer.observe(document.body, {
childList: true,
subtree: true
});

return observer;
}

// — Stripe Express Checkout Element (New Implementation) —
// Calculate the total for Express Checkout
function getTotalAmountForExpressCheckout() {
let subtotal = 0;
orderData.tickets.forEach(ticket => {
if (ticket.quantity > 0) {
subtotal += ticket.quantity * ticket.price;
}
});
let addOnsTotal = 0;
orderData.addOns.forEach(addon => {
addOnsTotal += addon.quantity * addon.price;
});
let totalAmount = subtotal + addOnsTotal;

// Apply voucher discount if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (discountMatch) {
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];
if (discountType === '%') {
totalAmount = subtotal * (1 – discountValue / 100) + addOnsTotal;
} else {
totalAmount = Math.max(0, subtotal – discountValue) + addOnsTotal;
}
}
}
return Math.round(totalAmount * 100); // in cents
}

// Create Payment Request Button (legacy API – works without clientSecret)
const paymentRequestAmount = getTotalAmountForExpressCheckout();
console.log('Creating Payment Request Button with amount:', paymentRequestAmount);

// Create payment request
const paymentRequest = stripe.paymentRequest({
country: 'IE',
currency: 'eur',
total: {
label: 'Fossetts Circus Tickets',
amount: paymentRequestAmount,
},
requestPayerName: true,
requestPayerEmail: true,
requestPayerPhone: true,
});

// Create payment request button
let paymentRequestButton;
try {
console.log('Creating payment request button…');
paymentRequestButton = elements.create('paymentRequestButton', {
paymentRequest: paymentRequest,
style: {
paymentRequestButton: {
type: 'default',
theme: 'dark',
height: '44px'
}
}
});
console.log('Payment Request Button created successfully:', paymentRequestButton);
} catch (error) {
console.error('Error creating Payment Request Button:', error);
console.error('Error details:', {
message: error.message,
stack: error.stack,
name: error.name
});
return;
}

// Check if payment request is supported
paymentRequest.canMakePayment().then(function(result) {
console.log('Payment Request canMakePayment result:', result);
// TEMPORARY: Always hide Google Pay and Apple Pay button for debugging
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
return; // Prevent any further logic from showing the button
// — original logic below (commented out) —
/*
if (result) {
console.log('Payment Request is supported, mounting button…');
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer && paymentRequestAmount > 0) {
paymentRequestContainer.innerHTML = '';
paymentRequestContainer.style.display = 'block';
try {
paymentRequestButton.mount('#payment-request-button');
console.log('Payment Request Button mounted successfully');
} catch (error) {
console.error('Error mounting Payment Request Button:', error);
}
} else {
console.log('Payment Request container not found or amount is 0');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
}
} else {
console.log('Payment Request is not supported');
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
}
*/
});

// Handle payment request events
paymentRequest.on('paymentmethod', async function(ev) {
console.log('Payment Request paymentmethod event:', ev);

try {
// Prepare form data
const form = document.getElementById('event-order-form');
const formData = new FormData(form);

// Add wheelchair user count
const wheelchairUsers = getWheelchairUserData();
if (wheelchairUsers > 0) {
formData.append('wheelchair_users', wheelchairUsers);
}

// Add payment token (backend expects payment_token)
formData.append('payment_token', ev.paymentMethod.id);

// Add event and date ids from URL parameters
const urlParams = getUrlParams();
const eventId = urlParams.event_id;
const eventDateId = urlParams.event_date_id;

// Add event and date ids to form data
formData.append('event_id', eventId);
formData.append('event_date_id', eventDateId);

// Add customer information from Payment Request
formData.append('customer_name', ev.payerName || '');
formData.append('customer_email', ev.payerEmail || '');
formData.append('customer_confirm_email', ev.payerEmail || '');
formData.append('customer_phone', ev.payerPhone || '');

// Collect ticket quantities (same as regular form submission)
console.log('Collecting ticket quantities for Payment Request…');
const sectionQuantities = {};
const quantityInputs = document.querySelectorAll('.quantity-value[data-section][data-ticket-type]');

quantityInputs.forEach(input => {
const section = input.dataset.section;
const ticketType = input.dataset.ticketType;
const quantity = parseInt(input.value) || 0;

if (!sectionQuantities[section]) {
sectionQuantities[section] = {};
}

if (ticketType && quantity > 0) {
sectionQuantities[section][ticketType.toLowerCase()] = quantity;
}

console.log('Processing ticket input:', {
section,
ticketType,
quantity,
value: input.value
});
});

console.log('Section quantities:', sectionQuantities);

// Send all ticket types dynamically with seating area information
Object.entries(sectionQuantities).forEach(([section, sectionData]) => {
Object.entries(sectionData).forEach(([ticketType, quantity]) => {
// Convert spaces to underscores in the ticket type name for the form field
// The server expects: event_seat_{seating_area}_{ticket_type_with_underscores}
const cleanTicketType = ticketType.replace(/s+/g, '_');
formData.append(`event_seat_${section}_${cleanTicketType}`, quantity);

console.log('Added form field:', `event_seat_${section}_${cleanTicketType}`, '=', quantity);
console.log('Ticket type details:', {
section: section,
originalTicketType: ticketType,
cleanTicketType: cleanTicketType,
quantity: quantity,
fieldName: `event_seat_${section}_${cleanTicketType}`
});
});
});

// Add add-ons
console.log('Collecting add-ons for Payment Request…');
const addonInputs = document.querySelectorAll('.addon-list-item .quantity-value');
console.log('Found addon inputs:', addonInputs.length);

addonInputs.forEach(input => {
const addonId = input.dataset.addonId;
const addonName = input.dataset.addonName;
const quantity = parseInt(input.value) || 0;

console.log('Processing addon input:', {
addonId,
addonName,
quantity,
value: input.value
});

if (addonId && quantity > 0) {
formData.append(`addon_${addonId}`, quantity);
console.log(`Added addon_${addonId} = ${quantity}`);
}
});

// Add voucher if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const voucherMatch = voucherText.match(/Voucher: ([A-Z0-9]+)/);
if (voucherMatch) {
formData.append('voucher_code', voucherMatch[1]);
}
}

// Submit order to server
const response = await fetch(`/order.php?event_id=${eventId}&event_date_id=${eventDateId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});

const result = await response.json();
if (result.success) {
// Complete the payment request successfully
ev.complete('success');
window.location.href = result.redirect_url;
} else {
// Complete the payment request with error
ev.complete('fail');
showSpeechBubble(result.message || 'Payment failed. Please try again.');
}
} catch (error) {
console.error('Error handling Payment Request payment:', error);
// Complete the payment request with error
ev.complete('fail');
showSpeechBubble('Payment processing failed. Please try again.');
}
});

paymentRequest.on('cancel', function() {
console.log('Payment Request cancelled');
});

// Update Payment Request amount when order changes
function updatePaymentRequestAmount() {
const newAmount = getTotalAmountForExpressCheckout();
paymentRequest.update({
total: {
label: 'Fossetts Circus Tickets',
amount: newAmount,
}
});
}

// Store the update function globally for use in order updates
window.updatePaymentRequestAmount = updatePaymentRequestAmount;

stripeInitialized = true;
console.log('Stripe Payment Request Button initialized successfully');

// Set up observer to catch dynamically created Stripe elements
const stripeObserver = setupStripeAccessibilityObserver();

// Suppress specific Stripe accessibility warnings
const originalWarn = console.warn;
console.warn = function(…args) {
const message = args.join(' ');
if (message.includes('aria-hidden') && message.includes('Stripe')) {
// Suppress Stripe aria-hidden warnings
return;
}
originalWarn.apply(console, args);
};

// Fix accessibility warnings after all elements are mounted
setTimeout(() => {
fixStripeAccessibilityWarnings();
}, 1000);

} catch (error) {
console.error('Error initializing Stripe Express Checkout:', error);
throw error;
}
}
above is the code that generate credit card fields but user unable to type anything in it specially in facebook android app browser

Assistant
Thanks for sharing your full Stripe integration logic. The issue you're facing — **credit card fields not accepting input in the Facebook Android in-app browser** — is a **known limitation** caused by:

### 🧨 The Root Cause

**Facebook and Instagram's in-app browsers (especially on Android)** block or restrict interaction with “s, which Stripe Elements rely on. Specifically:

– Stripe Elements are injected via secure iframes.
– In-app browsers may:
– Block focus inside iframes.
– Disable autofill and inputs.
– Fail silently, making it look like the fields aren't working.

### ✅ Recommended Fixes

#### ✅ **1. Detect and Warn In-App Browser Users**
Detect when the user is using Facebook/Instagram and **show a warning with a button to open in the real browser.**

**👉 Add this detection code at the top of your “ block:**

function isInAppBrowser() {
    const ua = navigator.userAgent || navigator.vendor || window.opera;
    return /FBAN|FBAV|Instagram|FB_IAB/.test(ua);
}

if (isInAppBrowser()) {
    const warning = document.createElement('div');
    warning.style = 'position:fixed;top:0;left:0;width:100%;height:100%;background:#fff;z-index:99999;text-align:center;padding:30px;';
    warning.innerHTML = `
        <h2 style="color:#ed1e26">⚠️ This browser is not supported</h2>
        <p>Please open this page in Chrome, Safari, or your default browser to complete your payment.</p>
        <a href="${window.location.href}" target="_blank" style="margin-top:20px;display:inline-block;padding:10px 20px;background:#ed1e26;color:#fff;border-radius:5px;text-decoration:none;">Open in Browser</a>
    `;
    document.body.innerHTML = '';
    document.body.appendChild(warning);
}

> ✅ This works immediately and avoids support calls or failed checkouts.

#### ✅ **2. Add `

<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'self' https://js.stripe.com;">

> Note: Not a guaranteed fix, but improves compatibility in some contexts.

#### ✅ **3. Long-Term Solution: Use Stripe Hosted Checkout**
Instead of Elements, use Stripe’s [Hosted Checkout](https://stripe.com/docs/checkout) — a full redirect to `checkout.stripe.com`:

– Completely avoids in-app browser issues.
– Handles 3D Secure automatically.
– Still allows coupon, tax, metadata, etc.

> You can conditionally redirect in-app users to Hosted Checkout while keeping Elements for normal browsers.

### ✅ Quick Summary

| Solution | Effectiveness | Time to Implement |
|——————————————|—————|——————-|
| 🟢 Detect in-app browser + show overlay | ✅ Works now | ⏱ 5 min |
| 🟢 Use Stripe Hosted Checkout redirect | ✅ Bulletproof | ⏱ 15-30 min |
| 🟡 CSP tweak (optional) | ⚠️ May help | ⏱ 2 min |

Would you like help switching only **in-app visitors to Hosted Checkout** while others use Elements?

User
How to apply hosted checkout

<script>
window.location='https://fossettscircus.com/success/?eid=" . $order_info->event_date_id . "&oc=" . $order_info->order_code . "';
</script>

' ";
goto continue_with_wordpress;
exit();
}
else
{
sendErrorMail( "Order not saved", "Order not saved but charge paid" . var_export( $order_info, true ) );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Order could not be saved'
]);
exit;
}
}
}
elseif ($intent->status == 'processing') {
echo json_encode([
'success' => false,
'message' => 'Payment is processing. Please wait a moment and try again.',
'status' => 'processing'
]);
exit;
} elseif ($intent->status == 'requires_payment_method') {
echo json_encode([
'success' => false,
'message' => 'Payment method failed. Please try again with a different card.',
'status' => 'requires_payment_method'
]);
exit;
} elseif ($intent->status == 'requires_confirmation') {
echo json_encode([
'success' => false,
'message' => 'Payment requires confirmation. Please try again.',
'status' => 'requires_confirmation'
]);
exit;
} else {
echo json_encode([
'success' => false,
'message' => 'Payment failed: ' . $intent->status,
'status' => $intent->status,
'errors' => ['There was an error during the transaction, charge was not paid.']
]);
exit;
}
}
catch(StripeErrorCard $e)
{
$body = $e->getJsonBody();
$err = $body['error'];

array_push( $order_errors, $err['message'] );

if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => $err['message'],
'errors' => $order_errors
]);
exit;
}
}
catch (Exception $e)
{
if ( is_object( $e ) && method_exists( $e, 'getJsonBody' ) )
{
$body = $e->getJsonBody();
$err = $body['error'];
array_push( $order_errors, $err['message'] );
}

array_push( $order_errors, 'There was an error during the transaction. Payment not performed.' );

sendErrorMail( "Exception", var_export( $e, true ) );

if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Payment processing error',
'errors' => $order_errors
]);
exit;
}
}
} // /Process payment

} // /Form submitted

?>

<style>
<?php echo file_get_contents( __DIR__ . '/css/style.css'); ?>
/*.div-wrapper{
    padding: 0em 1em !important;
}*/
.payment-fields {
    display: block !important;
    padding: 1em !important;
    border-radius: 5px;
    width: 100%;
    height: 3em;
    color: rgb(66, 109, 143);
    background-color: rgb(255,255,255);
    font-weight: bold;
}
</style>
<script>
<?php echo file_get_contents( __DIR__ . '/js/hashes.js'); ?>
</script>
<script>

</script>
<script>
var event_seat_location_options;
    var eventData = <?php echo json_encode($event_data); ?>;

    jQuery(document).ready(function() {
        if (!eventData || !eventData.event_dates) {
            console.error('Event data not available');
            return;
        }

        var latest_available_seat = jQuery('#event-order [name="event_seat_location"] option:selected').val();
        event_seat_location_options = jQuery('#event-order [name="event_seat_location"]').html();
        jQuery('#event-order [name="event_seat_location"]').html('<option value="">Pick a seat location</option>');
        updateAvailableLocations();
        jQuery('#event-order [name="event_seat_location"]').val(latest_available_seat);
    });
    
    function updateFormSent()
    {
        var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
        jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').removeAttr('disabled');
        jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').show();
        updateTotal();
    }

    function updateAvailableLocations() {
        if (!eventData || !eventData.event_dates) {
            console.error('Event data not available');
            return;
        }

        // reset options
        jQuery('#event-order [name="event_seat_location"]').html(event_seat_location_options);

        var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
        
        jQuery('#event-order [name="event_seat_location"] option').prop('disabled', true).hide();
        jQuery('#event-order [name="event_seat_location"] option:first').prop('disabled', false).show();
        jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').prop('disabled', false).show();
        
        // remove unwanted options
        jQuery('#event-order [name="event_seat_location"]').find('option:disabled').remove();

        jQuery('#event-order [name="event_seat_location"]').val('');
    }

    function updateAvailableSeats()
    {
        var seatsLeft = parseInt( jQuery('#event-order [name="event_seat_location"] option:selected').attr('data-left') );

        var seatsSelected = parseInt( jQuery('#event-order [name="event_seat_full"]').val() ) + parseInt( jQuery("#event-order [name='event_seat_child']").val() ) + parseInt( jQuery("#event-order [name='event_seat_senior']").val() );
    
        if ( seatsSelected > seatsLeft )
        {
            jQuery('#event-order .warning-seats').html('You selected more tickets than are available right now in ' + jQuery("#event-order [name='event_seat_location'] option:selected").text() + ', please pick a different location or reduce the number of tickets by ' + ( seatsSelected - seatsLeft ) + '.' );
            jQuery('#event-order .warning-seats').show();
            return false;
        }
        else
        {
            jQuery('#event-order .warning-seats').hide();
            jQuery('#event-order .warning-seats').html('');
            return true;
        }
    }

    var V = JSON.parse( '<?php echo $event_data->event_vouchers; ?>' );
    var D = 0;
    var T = '';

    function voucherValid()
    {
        var SHA1 = new Hashes.SHA1;

        var customer_voucher = SHA1.hex( jQuery("#event-order [name='voucher']").val().toLowerCase() );

        for (var i = 0; i < V.length; i++)
        {
            if ( customer_voucher == V[i].voucher_code )
            {
                D = V[i].voucher_discount;
                T = V[i].voucher_discount_type;
                return true;
            }
        }

        return false;
    }

    function updateTotal()
    {
        var seatsFull = parseInt( jQuery('#event-order [name="event_seat_full"]').val() );
        var seatsChild = parseInt( jQuery('#event-order [name="event_seat_child"]').val() );
        var seatsSenior = parseInt( jQuery('#event-order [name="event_seat_senior"]').val() );
    
        var priceFull = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-full' ) );
        var priceReduced = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-reduced' ) );
    
        var total = ( seatsFull * priceFull ) + ( ( seatsChild + seatsSenior ) * priceReduced );

        if ( voucherValid() )
        {
            if ( T == 'percentage' )
            {
                total = total * ( 1 - ( D / 100 ) );
            }
            else if ( T == 'amount' )
            {
                total = total - D;
            }
        }
        
        var currency = '<?php echo $currency_symbol; ?>';

        if ( isNaN( total ) )
        {
            jQuery('#event-order .total').html('');
        }
        else
        {
            jQuery('#event-order .total').html('Total: '+ currency + total );
        }
    }

    jQuery(document).ready(function()
    {
        /*var stripe = Stripe('pk_test_VBObL72a6bVf4DAEpzOKvlh1', { // thenet
           stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
        });*/
        
        var stripe = Stripe('pk_live_KSh8MRi76Kliuk4fUpBZ23GS', { // thenet live mode
            stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
        });
        
    <?php if ( isset( $_POST['payment_intent_client_secret'] ) ): ?>
    
        stripe.handleCardAction(
            '<?php echo $_POST['payment_intent_client_secret']; ?>'
        ).then(function(result)
        {
            if (result.error)
            {
                jQuery('#event-order .warning-payment').html( result.error.message );
                jQuery('#event-order .warning-payment').show();
            }
            else
            {
                jQuery('[name="payment_intent_id"]').val( result.paymentIntent.id  );
                jQuery('#event-order .warning-payment').hide();
                jQuery('#event-order .warning-payment').html( '' );
                jQuery("#event-order #event-order-form").trigger('submit');
            }
        });

    <?php else: ?>

        var elements = stripe.elements();

        var paymentStyle = {
            base: {
                color: 'rgb(66, 109, 143)',
                '::placeholder': {
                    color: 'rgba(66, 109, 143, 0.7)',
                },
            }
        };

        // var card = elements.create('card', { hidePostalCode: true, style: paymentStyle });
        var card = elements.create('cardNumber', { showIcon: true, style: paymentStyle });
        var cardExpiry = elements.create('cardExpiry', { style: paymentStyle });
        var cardCvc = elements.create('cardCvc', { style: paymentStyle });

        card.addEventListener('change', function( event )
        {
            if ( event.error )
            {
                jQuery('#event-order .warning-payment').html( event.error.message );
                jQuery('#event-order .warning-payment').show();
            }
            else
            {
                jQuery('#event-order .warning-payment').hide();
                jQuery('#event-order .warning-payment').html( '' );
            }
        });

        // card.mount('#event-order #payment-fields');
        
        card.mount('#event-order #card-number');
        cardExpiry.mount('#event-order #card-expiry');
        cardCvc.mount('#event-order #card-cvc');
        
    <?php endif; ?>

        jQuery("#event-order [name='event_datetime']").on("change", function()
        {
            updateAvailableLocations();
            updateTotal();
        });

        jQuery("#event-order [name='event_seat_location'], #event-order [name='event_seat_full'], #event-order [name='event_seat_child'], #event-order [name='event_seat_senior']").on("change", function()
        {
            updateAvailableSeats();
            updateTotal();
        });

        jQuery("#event-order [name='voucher']").on('input',function()
        {
            if ( voucherValid() )
            {
                jQuery('.warning-voucher').hide();
                jQuery('.success-voucher').show();
            }
            else
            {
                if ( jQuery(this).val() == '' )
                {
                    jQuery('.success-voucher').hide();
                    jQuery('.warning-voucher').hide();
                }
                else
                {
                    jQuery('.success-voucher').hide();
                    jQuery('.warning-voucher').show();
                }
            }

            updateTotal();
        });

        jQuery("#event-order .order-submit").on('click', function()
        {
            jQuery("#event-order .order-submit").prop('disabled', true);

            if ( updateAvailableSeats() )
            {
                // Stripe SCA
                stripe.createPaymentMethod('card', card, {
                    billing_details: { name: jQuery('[name="customer_name"]').val() }
                })
                //stripe.createToken( card )
                .then( function(result)
                {
                    if ( result.error )
                    {
                        jQuery('#event-order .warning-payment').html( result.error.message );
                        jQuery('#event-order .warning-payment').show();
                        jQuery("#event-order .order-submit").prop('disabled', false);
                    }
                    else
                    {
                        jQuery('#event-order [name="payment_token"]').val( result.paymentMethod.id );
                        //jQuery('#event-order [name="payment_token"]').val( result.token.id );
                        jQuery('#event-order .warning-payment').hide();
                        jQuery('#event-order .warning-payment').html( '' );
                        jQuery("#event-order #event-order-form").trigger('submit');
                    }
                });
            }
            else
            {
                jQuery("#event-order .order-submit").prop('disabled', false);
            }
        });
        
        /*jQuery("#event-order #event-order-form").on("submit", function(e)
        {
            jQuery("#event-order .order-submit").trigger('click');
            e.preventDefault();
        });*/

        updateFormSent();
        
    });
</script>
<script>
jQuery(document).ready(function()
            {
                jQuery('html, body').animate(
                {
                    scrollTop: ( jQuery('.form-errors').first().offset().top - 10 )
                }, 500 );
            });
</script>
<script>
jQuery(document).ready(function()
        {
            function checkEmailValue(value, arr) {
                var status = 'not_exist';

                for (var i = 0; i < arr.length; i++) {
                    var name = arr[i];
                    if (name == value) {
                        status = 'exist';
                break;
            }
        }

                return status;
            }

            //code to check if a value exists in an array
            var email_pool = ['gmail.com','yahoo.com','hotmail.com','aol.com','hotmail.co.uk','hotmail.fr','msn.com','yahoo.fr','wanadoo.fr','orange.fr','comcast.net','yahoo.co.uk','yahoo.com.br','yahoo.co.in','live.com','rediffmail.com','free.fr','gmx.de','web.de','yandex.ru','ymail.com','libero.it','outlook.com','uol.com.br','bol.com.br','mail.ru','cox.net','hotmail.it','sbcglobal.net','sfr.fr','live.fr','verizon.net','live.co.uk','googlemail.com','yahoo.es','ig.com.br','live.nl','bigpond.com','terra.com.br','yahoo.it','neuf.fr','yahoo.de','alice.it','rocketmail.com','att.net','laposte.net','facebook.com','bellsouth.net','yahoo.in','hotmail.es','charter.net','yahoo.ca','yahoo.com.au','rambler.ru','hotmail.de','tiscali.it','shaw.ca','yahoo.co.jp','sky.com','earthlink.net','optonline.net','freenet.de','t-online.de','aliceadsl.fr','virgilio.it','home.nl','qq.com','telenet.be','me.com','yahoo.com.ar','tiscali.co.uk','yahoo.com.mx','voila.fr','gmx.net','mail.com','planet.nl','tin.it','live.it','ntlworld.com','arcor.de','yahoo.co.id','frontiernet.net','hetnet.nl','live.com.au','yahoo.com.sg','zonnet.nl','club-internet.fr','juno.com','optusnet.com.au','blueyonder.co.uk','bluewin.ch','skynet.be','sympatico.ca','windstream.net','mac.com','centurytel.net','chello.nl','live.ca','aim.com','bigpond.net.au'];

            jQuery( "#confirm_email_address" ).focusout(function() {

                jQuery("#confirm_email_error").css("display", (jQuery("#email_address").val() != jQuery("#confirm_email_address").val()) ? "inline-block" : "none");

                var user_email = jQuery("#email_address").val();

                var domain_name = user_email.indexOf("@");

                var slice_email = user_email.slice((domain_name + 1), user_email.length);

                if(jQuery("#email_address").val() == jQuery("#confirm_email_address").val()){
                    // if(!jQuery.inArray(slice_email, email_pool)){
                    if(checkEmailValue(slice_email, email_pool) == 'not_exist'){
                        jQuery("#confirm_email_error2").css("display", "inline-block");
                        jQuery("#confirm_email_error2").html("Hmmm, are you sure... <strong>"+user_email+"</strong> is the correct email address?");
                    }else{
                        jQuery("#confirm_email_error2").css("display", "none");
                    }
                }
            });

                    
        });
</script>
<style>
.email_address_error{
            margin-top: 5px !important;
            color: rgb(255, 255, 255);
            background-color: rgb(237, 30, 38);
            font-weight: normal;
            padding: 0.3em 15px;
        }.email_address_warning{
            margin-top: 5px !important;
            color: rgb(255, 255, 255);
            background-color: #25a828;
            font-weight: normal;
            padding: 0.3em 15px;
        }
</style>

Contact details

Assistant
Thanks for confirming. Since you're already using Stripe Elements and the in-app browser (e.g. Facebook Android) prevents interaction with the embedded iframes, the best immediate fix is to **detect the environment and prompt the user to open the site in their actual browser**.

### ✅ Final Solution: Detect In-App Browser + Block Stripe UI with Redirect Prompt

Here’s a clean, production-ready solution:

#### ✅ 1. Add this JS at the top of your checkout page:

<script>
(function() {
  const ua = navigator.userAgent || navigator.vendor || window.opera;
  const isInApp = /FBAN|FBAV|Instagram|FB_IAB/.test(ua);

  if (isInApp) {
    document.body.innerHTML = `
      <div style="position:fixed;top:0;left:0;width:100%;height:100%;background:#fff;z-index:999999;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:2rem;text-align:center;">
        <h2 style="color:#ed1e26;margin-bottom:1rem;">⚠️ Secure Checkout Not Supported Here</h2>
        <p style="max-width:400px;font-size:16px;color:#333;margin-bottom:2rem;">
          Facebook or Instagram’s browser doesn’t allow secure credit card entry.<br>
          Please open this page in your device’s browser to complete your payment.
        </p>
        <a href="${window.location.href}" target="_blank" rel="noopener" style="background:#ed1e26;color:#fff;text-decoration:none;padding:12px 24px;border-radius:6px;font-size:16px;">
          🔓 Open in Browser
        </a>
      </div>
    `;
  }
})();
</script>

### 🧪 Test Scenarios

| Environment | What Happens |
|—————————–|—————————————–|
| Facebook or Instagram app | Overlay blocks Stripe fields, suggests opening in browser |
| Chrome, Safari, Edge, etc. | Stripe Elements work normally |

### 📌 Why This Works

– Stripe Elements are injected via secure “s.
– Facebook’s in-app browser blocks user interaction with those iframes.
– This script gracefully handles the scenario without confusing the user or failing silently.

Would you like to conditionally **redirect those users to a Stripe Hosted Checkout link** instead of showing Elements at all? I can help you with that logic too.

User
I want to change the flow and use hosted checkout stripe
Assistant
Perfect — switching to **Stripe Hosted Checkout** is the cleanest and most robust solution, especially for avoiding issues with Facebook, Instagram, and other restrictive in-app browsers.

### ✅ What Is Stripe Hosted Checkout?

It’s a **Stripe-hosted payment page** that you redirect the user to. It:
– Works seamlessly across all devices and browsers (even in-app).
– Handles all card fields, validation, and 3D Secure.
– Can be customized with your brand.
– Returns the user back to your site after success/cancel.

### ✅ Your New Flow

1. User clicks **"Pay Now"** or similar.
2. You send order data to your backend (via AJAX or form).
3. Backend creates a **Stripe Checkout Session** via API.
4. Stripe returns a `sessionId`.
5. Frontend redirects to Stripe's hosted page using `stripe.redirectToCheckout({ sessionId })`.

### 🔧 Step-by-Step Implementation

#### ✅ 1. Install Stripe PHP SDK (if not already)

composer require stripe/stripe-php

#### ✅ 2. Create the Checkout Session (server-side)

Create a new PHP endpoint like `create_checkout_session.php`:

<?php
require 'vendor/autoload.php'; // Adjust path if needed

StripeStripe::setApiKey('sk_live_...'); // Your secret key

$YOUR_DOMAIN = 'https://example.com'; // Change to your domain

// Example ticket data from frontend
$amountInEuro = $_POST['amount'] ?? 20.00; // Always validate!
$quantity = $_POST['quantity'] ?? 1;
$eventId = $_POST['event_id'] ?? 'unknown_event';

$session = StripeCheckoutSession::create([
    'payment_method_types' => ['card'],
    'line_items' => [[
        'price_data' => [
            'currency' => 'eur',
            'product_data' => [
                'name' => 'Fossetts Circus Ticket',
                'metadata' => [
                    'event_id' => $eventId,
                ],
            ],
            'unit_amount' => $amountInEuro * 100,
        ],
        'quantity' => $quantity,
    ]],
    'mode' => 'payment',
    'success_url' => $YOUR_DOMAIN . '/success.php?session_id={CHECKOUT_SESSION_ID}',
    'cancel_url' => $YOUR_DOMAIN . '/cancel.php',
]);

echo json_encode(['sessionId' => $session->id]);

#### ✅ 3. Frontend: Redirect to Hosted Checkout

In your JavaScript:

const stripe = Stripe('pk_live_...');

document.getElementById('checkout-button').addEventListener('click', async () => {
  const formData = new FormData(document.getElementById('event-order-form'));

  const response = await fetch('/create_checkout_session.php', {
    method: 'POST',
    body: formData
  });

  const result = await response.json();

  if (result.sessionId) {
    stripe.redirectToCheckout({ sessionId: result.sessionId });
  } else {
    alert('Could not start checkout. Please try again.');
  }
});

#### ✅ 4. Show Thank You / Success Page

In `success.php`, you can confirm the session:

$sessionId = $_GET['session_id'];

$session = StripeCheckoutSession::retrieve($sessionId);

// You can now verify the order and mark it as paid

### ✅ Benefits

| Feature | Hosted Checkout |
|——————————-|—————–|
| Secure credit card handling | ✅ |
| 3D Secure & Apple/Google Pay | ✅ |
| In-app browser compatibility | ✅ |
| Automatic receipt emails | ✅ |
| VAT & tax support | ✅ |
| Branding options | ✅ |

Would you like me to help refactor your existing `order.php` to use this hosted checkout flow, while preserving all your ticket/add-on logic?

User
let orderData = {
tickets: [],
addOns: [],
total: 0
};

// Store event date string globally for use in order summary
let selectedEventDateString = '';

// Global add-ons data for label lookup
let globalAddOnsData = [];

// Cache DOM elements
const domCache = {
form: null,
ticketSections: null,
addOnsGrid: null,
cart: null
};

// Initialize DOM cache
function initializeDomCache() {
domCache.form = document.getElementById('event-order-form');
domCache.ticketSections = document.getElementById('ticket-sections');
domCache.addOnsGrid = document.getElementById('add-ons-grid');
domCache.cart = document.getElementById('cart');
}

function updateOrderSummary() {
console.log('updateOrderSummary called');
console.log('Current orderData:', orderData);

if (!domCache.form) return;

// Update event date/time in order summary (step 3)
const summaryEventDate = document.getElementById('summary-event-date');
if (summaryEventDate) {
summaryEventDate.textContent = selectedEventDateString || '';
}

let subtotal = 0;
let ticketData = [];

// Process each ticket
orderData.tickets.forEach(ticket => {
if (ticket.quantity > 0) {
const itemSubtotal = ticket.quantity * ticket.price;
subtotal += itemSubtotal;
ticketData.push({
section: ticket.section,
type: ticket.type,
quantity: ticket.quantity,
price: ticket.price,
subtotal: itemSubtotal
});
}
});

// Calculate add-ons total
const addOnsTotal = calculateAddOns();
let total = subtotal + addOnsTotal;

// Apply voucher discount if valid – only to tickets
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const voucherMatch = voucherText.match(/Voucher Applied: (w+)/);
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (voucherMatch && discountMatch) {
const voucherCode = voucherMatch[1];
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];

if (discountType === '%') {
// Apply discount only to ticket subtotal
const discountedTicketTotal = subtotal * (1 – discountValue / 100);
total = discountedTicketTotal + addOnsTotal;
} else {
// For fixed amount, apply to ticket total only
const discountedTicketTotal = Math.max(0, subtotal – discountValue);
total = discountedTicketTotal + addOnsTotal;
}
}
}

// Update cart display
updateCartDisplay(ticketData, subtotal, addOnsTotal, total);
saveOrderDataToStorage();
addRemoveListenersToSummary();

// Update Express Checkout amount if available
if (window.updatePaymentRequestAmount) {
window.updatePaymentRequestAmount();
}
}

function calculateSubtotal() {
let subtotal = 0;
const ticketSelects = document.querySelectorAll('select[name^="event_seat_"]');

for (const select of ticketSelects) {
subtotal += parseInt(select.value) * parseFloat(select.dataset.price);
}

return subtotal;
}

function calculateAddOns() {
let addOnsTotal = 0;
orderData.addOns.forEach(addon => {
addOnsTotal += addon.quantity * addon.price;
});
return addOnsTotal;
}

function generateOrderSummaryHTML() {
let html = '';

// Add tickets to summary
if (orderData?.tickets?.length > 0) {
html += orderData.tickets
.filter(ticket => ticket?.type && ticket?.price && ticket?.quantity)
.map(ticket => `

${ticket.type} Ticket
€${ticket.price.toFixed(2)} each

x${ticket.quantity}
€${(ticket.price * ticket.quantity).toFixed(2)}

`).join('');
}

// Add wheelchair count to summary
const wheelchairUsers = getWheelchairUserData();
if (wheelchairUsers > 0) {
html += `

Wheelchair
Access required

x${wheelchairUsers}

`;
}

// Add add-ons to summary
if (orderData?.addOns?.length > 0) {
console.log('Generating add-ons summary for:', orderData.addOns);
html += orderData.addOns
.filter(addon => addon?.name && addon?.price && addon?.quantity)
.map(addon => {
console.log('Processing addon:', addon);
const displayName = addon.label || addon.name;
console.log('Display name:', displayName);
return `

${displayName}
€${addon.price.toFixed(2)} each

x${addon.quantity}
€${(addon.price * addon.quantity).toFixed(2)}

`;
}).join('');
}

// If cart is empty, show message
if (!orderData?.tickets?.length && !orderData?.addOns?.length && wheelchairUsers === 0) {
html = `

Your cart is empty
Select tickets to continue

`;
} else {
// Add total if there are items
const total = orderData.total || 0;
html += `

Total:
€${total.toFixed(2)}

`;
}

return html;
}

function saveOrderData(orderData) {
localStorage.setItem('orderData', JSON.stringify(orderData));

if (domCache.form) {
// Update ticket quantities
if (Array.isArray(orderData.tickets)) {
orderData.tickets.forEach(ticket => {
const select = domCache.form.querySelector(`[name="event_seat_${ticket.type.toLowerCase()}"]`);
if (select) select.value = ticket.quantity;
});
}

// Update add-ons
if (Array.isArray(orderData.addOns)) {
orderData.addOns.forEach(addon => {
const select = domCache.form.querySelector(`[name="addon_${addon.name}"]`);
if (select) select.value = addon.quantity;
});
}
}

// Update cart display without triggering saveOrderData
if (Array.isArray(orderData.tickets)) {
updateCartDisplay(
orderData.tickets,
orderData.total – (orderData.addOns?.reduce((sum, addon) => sum + (addon.price * addon.quantity), 0) || 0),
orderData.addOns?.reduce((sum, addon) => sum + (addon.price * addon.quantity), 0) || 0,
orderData.total,
false
);
}
}

// Add this function near the top-level functions
function saveOrderDataToStorage() {
try {
localStorage.setItem('fossettsOrderData', JSON.stringify(orderData));
} catch (e) {
console.error('Failed to save order data:', e);
}
}

function restoreSelectionsToUI() {
// Restore ticket quantities
if (orderData.tickets && Array.isArray(orderData.tickets)) {
orderData.tickets.forEach(ticket => {
// Use lowercase for ticket type to match attribute
const input = document.querySelector(`.quantity-value[data-section="${ticket.section}"][data-ticket-type="${ticket.type.toLowerCase()}"]`);
if (input) {
input.value = ticket.quantity;
}
});
}
// Restore add-on quantities
if (orderData.addOns && Array.isArray(orderData.addOns)) {
orderData.addOns.forEach(addon => {
const input = document.querySelector(`.quantity-value[data-addon-name="${addon.name}"]`);
if (input) {
input.value = addon.quantity;
}
});
}
}

// On DOMContentLoaded, load orderData from localStorage if present
const savedOrderData = localStorage.getItem('fossettsOrderData');
if (savedOrderData) {
try {
orderData = JSON.parse(savedOrderData);
restoreSelectionsToUI();
updateOrderSummary();
// Optionally, update UI for tickets/addons here if needed
} catch (e) {
console.error('Failed to parse saved order data:', e);
}
}

// Global function to show speech bubble warning
function showSpeechBubble(message) {
console.log('Showing speech bubble:', message);
const speechBubble = document.querySelector('.speech-bubble-warning');
if (!speechBubble) {
console.error('Speech bubble element not found');
return;
}

// Determine which step is active and position accordingly
const activeStep = document.querySelector('.step_1.active, .step_2.active, .step_3.active');
let nextButton = document.querySelector('.sticky-cart-bar .next-step');

// For Step 3, target the Buy Now button if it exists
if (activeStep && activeStep.classList.contains('step_3')) {
const buyNowButton = document.querySelector('.buy-now-btn.complete-purchase');
if (buyNowButton && buyNowButton.style.display !== 'none') {
nextButton = buyNowButton;
}
}

console.log('Active step:', activeStep?.className);
console.log('Next button found:', nextButton);
if (nextButton) {
console.log('Next button position:', nextButton.getBoundingClientRect());
}

// Use the same fixed positioning for both desktop and mobile
speechBubble.style.position = 'fixed';
speechBubble.style.bottom = '100px';
speechBubble.style.right = '10px';
speechBubble.style.top = 'auto';
speechBubble.style.left = 'auto';
speechBubble.style.transform = 'none';
speechBubble.style.zIndex = '9999';

// Move bubble to body
if (speechBubble.parentElement !== document.body) {
document.body.appendChild(speechBubble);
}

speechBubble.textContent = message;
speechBubble.style.display = 'block';

// No need to scroll since bubble is in fixed position

// Hide after 3 seconds
setTimeout(() => {
speechBubble.style.display = 'none';
}, 3000);
}

// Update the populateTicketSections function to include order info
function populateTicketSections(eventData) {
if (!domCache.ticketSections) return;

// Update order info block
const orderInfoBlock = document.querySelector('.order-info-block');
if (orderInfoBlock) {
orderInfoBlock.innerHTML = `
Order Information

${eventData.event_date_string}

${eventData.venue_title}

${eventData.venue_full}

`;
}

const html = eventData.tickets.map(section => {
const sectionId = section.title.toLowerCase().replace(/s+/g, '_');

return `

${section.title}
${section.seating_type}

Ticket Type
Price Per Person
Number of Tickets*

${section.ticket_types.map(type => `

${type.label}

Online Special
€${type.price}

+

`).join('')}

`}).join('');

domCache.ticketSections.innerHTML = html;

// Add event listeners for quantity buttons
document.querySelectorAll('.ticket-quantity-btn').forEach(button => {
button.addEventListener('click', function() {
const section = this.dataset.section;
const ticketType = this.dataset.ticketType;
const price = parseFloat(this.dataset.price);
const quantityInput = document.querySelector(`.quantity-value[data-section="${section}"][data-ticket-type="${ticketType}"]`);
const currentValue = parseInt(quantityInput.value);

if (this.classList.contains('minus') && currentValue > 0) {
quantityInput.value = currentValue – 1;
updateTicketQuantity(section, ticketType, price, currentValue – 1);
} else if (this.classList.contains('plus')) {
const maxValue = parseInt(quantityInput.getAttribute('max'));
if (currentValue < maxValue) {
quantityInput.value = currentValue + 1;
updateTicketQuantity(section, ticketType, price, currentValue + 1);
}
}
});
});
}

function populateAddOns(eventData) {
if (!domCache.addOnsGrid) return;
// Build all HTML at once
const html = `

${eventData.add_ons.map(addon => `

${addon.label}
${addon.description ? `${addon.description}` : ''}
€${addon.price.toFixed(2)}

+

`).join('')}
`;
domCache.addOnsGrid.innerHTML = html;
// Add event listeners for quantity buttons
document.querySelectorAll('.addon-list-item .quantity-btn').forEach(button => {
button.addEventListener('click', function() {
const addonName = this.dataset.addonName;
const price = parseFloat(this.dataset.price);
const quantityInput = document.querySelector(`.addon-list-item .quantity-value[data-addon-name="${addonName}"]`);
const currentValue = parseInt(quantityInput.value);
if (this.classList.contains('minus') && currentValue > 0) {
quantityInput.value = currentValue – 1;
updateAddonQuantity(addonName, price, currentValue – 1);
} else if (this.classList.contains('plus')) {
const maxValue = parseInt(quantityInput.getAttribute('max'));
if (currentValue < maxValue) {
quantityInput.value = currentValue + 1;
updateAddonQuantity(addonName, price, currentValue + 1);
}
}
});
});
}

function checkAndHandleAddOns(eventData) {
// If add_ons is empty or all are disabled, hide step_2
const hasActiveAddOns = eventData.add_ons && eventData.add_ons.some(addon => addon.enabled !== false);
const step2 = document.querySelector('.step_2');
if (!hasActiveAddOns) {
if (step2) {
step2.classList.add('inactive');
step2.style.display = 'none';
}
window.skipAddOnsStep = true;
} else {
if (step2) {
step2.classList.remove('inactive');
step2.style.display = '';
}
window.skipAddOnsStep = false;
}
}

function updateAddonQuantity(addonName, price, quantity) {
console.log('updateAddonQuantity called with:', { addonName, price, quantity });
console.log('globalAddOnsData:', globalAddOnsData);

// Find or create addon in orderData
const addonIndex = orderData.addOns.findIndex(a => a.name === addonName);

if (addonIndex === -1 && quantity > 0) {
// Find the addon data to get the proper label
const addonData = globalAddOnsData.find(addon => addon.name === addonName);
const label = addonData?.label || addonName;

console.log('Found addon data:', addonData);
console.log('Using label:', label);

// Add new addon
orderData.addOns.push({
name: addonName,
label: label,
price: price,
quantity: quantity
});
} else if (addonIndex !== -1) {
if (quantity > 0) {
orderData.addOns[addonIndex].quantity = quantity;
} else {
// Remove addon if quantity is zero
orderData.addOns.splice(addonIndex, 1);
}
}

// Update cart display
updateOrderSummary();
saveOrderDataToStorage();
}

// Event delegation for ticket and add-on selects
function handleSelectChange(event) {
if (event.target.matches('select[name^="event_seat_"], .add-on-quantity select')) {
updateOrderSummary();
}
}

// Initialize event listeners using event delegation
function initializeEventListeners() {
if (domCache.form) {
domCache.form.addEventListener('change', handleSelectChange);
// Add form submission handler
domCache.form.addEventListener('submit', handleFormSubmit);
}

// Add click handler for complete purchase button
const completePurchaseBtn = document.querySelector('.next-step.complete-purchase');
if (completePurchaseBtn) {
completePurchaseBtn.addEventListener('click', function(event) {
event.preventDefault();
handleFormSubmit(event);
});
}

// Add voucher validation
const applyVoucherBtn = document.querySelector('.apply-voucher-btn');
const voucherInput = document.querySelector('input[name="voucher"]');
const warningVoucher = document.querySelector('.warning-voucher');
const successVoucher = document.querySelector('.success-voucher');

if (applyVoucherBtn && voucherInput) {
applyVoucherBtn.addEventListener('click', async function() {
const voucherCode = voucherInput.value.trim();

if (!voucherCode) {
warningVoucher.style.display = 'none';
successVoucher.style.display = 'none';
return;
}

try {
// Get event IDs from URL
const urlParams = new URLSearchParams(window.location.search);
const eventId = urlParams.get('event_id');
const eventDateId = urlParams.get('event_date_id');

if (!eventId) {
throw new Error('Missing event information');
}

// Call backend to validate voucher
const response = await fetch(`/api/validate-voucher.php?event_id=${eventId}&voucher_code=${encodeURIComponent(voucherCode)}`);
const result = await response.json();

if (result.valid) {
warningVoucher.style.display = 'none';
successVoucher.style.display = 'block';
successVoucher.textContent = `Voucher Applied: ${voucherCode} – Discount on tickets ${result.discount_type === 'percentage' ? '-' + result.discount + '%' : '-€' + result.discount}`;
// Update total with discount
updateOrderSummary();
} else {
warningVoucher.style.display = 'block';
successVoucher.style.display = 'none';
}
} catch (error) {
console.error('Error validating voucher:', error);
warningVoucher.style.display = 'block';
successVoucher.style.display = 'none';
}
});

// Clear messages when input changes
voucherInput.addEventListener('input', function() {
warningVoucher.style.display = 'none';
successVoucher.style.display = 'none';
});
}
}

// Main initialization
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Starting initialization…');
initializeDomCache();
initializeEventListeners();

// Initialize speech bubble warning
const speechBubble = document.querySelector('.speech-bubble-warning');
if (speechBubble) {
speechBubble.style.display = 'none';
}

// Add form submission handler
const form = document.getElementById('event-order-form');
if (form) {
form.addEventListener('submit', handleFormSubmit);
}

// Add click handler for complete purchase button
const completePurchaseBtn = document.querySelector('.next-step.complete-purchase');
if (completePurchaseBtn) {
completePurchaseBtn.addEventListener('click', function(event) {
event.preventDefault();
handleFormSubmit(event);
});
}

// Initialize step navigation
const params = getUrlParams();
if (params.event_id && params.event_date_id) {
fetchEventData(params.event_id, params.event_date_id)
.then(eventData => {
checkAndHandleAddOns(eventData);
populateTicketSections(eventData);
populateAddOns(eventData);
restoreSelectionsToUI(); // {
console.error('Error loading event data:', error);

// Check if it's a booking closed error
if (error.message && error.message.includes('Online Booking Closed')) {
// Get the event date from the URL parameters to calculate countdown
const params = getUrlParams();
const url = `/api/events.php?event_id=${params.event_id}&event_date_id=${params.event_date_id}`;

fetch(url)
.then(response => response.json())
.then(data => {
const eventDate = data.event_dates.find(date => date.event_date_id === parseInt(params.event_date_id));
if (eventDate) {
const showtime = new Date(eventDate.event_datetime);
const now = new Date();
const timeDiff = showtime – now;
const minutesUntilShow = Math.floor(timeDiff / (1000 * 60));

// Display online closed message with countdown
const ticketSections = document.getElementById('ticket-sections');
if (ticketSections) {
ticketSections.innerHTML = `

ONLINE CLOSED – Starts in ${minutesUntilShow} min

Go to Boxoffice

Box Office Information:
• Open 1 hour before showtime
• Cash and card payments accepted
• Subject to availability

`;
}
}
})
.catch(() => {
// Fallback if we can't get the countdown
const ticketSections = document.getElementById('ticket-sections');
if (ticketSections) {
ticketSections.innerHTML = `

ONLINE CLOSED

This show is starting within 30 minutes.

Go to Boxoffice

`;
}
});
} else {
// Show generic error message
showSpeechBubble('Error loading event data. Please refresh the page.');
}
});
}

// Wheelchair user logic (number input with +/-)
const wheelchairCountInput = document.getElementById('wheelchair-user-count');
const wheelchairMinus = document.getElementById('wheelchair-minus');
const wheelchairPlus = document.getElementById('wheelchair-plus');
if (wheelchairCountInput && wheelchairMinus && wheelchairPlus) {
wheelchairMinus.addEventListener('click', function() {
let val = parseInt(wheelchairCountInput.value) || 0;
if (val > 0) wheelchairCountInput.value = val – 1;
});
wheelchairPlus.addEventListener('click', function() {
let val = parseInt(wheelchairCountInput.value) || 0;
wheelchairCountInput.value = val + 1;
});
}
});

function getWheelchairUserData() {
const wheelchairCountInput = document.getElementById('wheelchair-user-count');
let wheelchairUsers = 0;
if (wheelchairCountInput) {
wheelchairUsers = parseInt(wheelchairCountInput.value) || 0;
}
return wheelchairUsers;
}

// In handleFormSubmit or wherever orderData is finalized before sending to backend:
// orderData.wheelchair_users = getWheelchairUserData();

// Cart functionality
let cartInitialized = false;
let isToggling = false;

function toggleCart(event) {
if (isToggling) return;
isToggling = true;

if (event) {
event.preventDefault();
event.stopPropagation();
}

console.log('Toggling cart', {
cartInitialized,
event: event ? event.type : 'manual',
target: event ? event.target : 'none'
});

const cartSlideOut = document.querySelector('.cart-slide-out');
const cartToggle = document.querySelector('.cart-toggle');
const cartToggleIcon = document.querySelector('.cart-toggle-icon');

if (!cartSlideOut || !cartToggle) {
console.error('Cart elements not found:', { cartSlideOut, cartToggle });
isToggling = false;
return;
}

const isActive = cartSlideOut.classList.contains('active');

if (isActive) {
cartSlideOut.classList.remove('active');
cartToggle.classList.remove('active');
if (cartToggleIcon) cartToggleIcon.classList.remove('active');
} else {
cartSlideOut.classList.add('active');
cartToggle.classList.add('active');
if (cartToggleIcon) cartToggleIcon.classList.add('active');
}

setTimeout(() => {
isToggling = false;
}, 100);
}

function initializeCart() {
if (cartInitialized) {
console.log('Cart already initialized');
return;
}

console.log('Initializing cart functionality');

// Remove any existing event listeners
const cartToggle = document.querySelector('.cart-toggle');
const innerCartToggle = document.querySelector('.cart-toggle.inner');

if (cartToggle) {
cartToggle.removeEventListener('click', toggleCart);
cartToggle.addEventListener('click', toggleCart);
}

if (innerCartToggle) {
innerCartToggle.removeEventListener('click', toggleCart);
innerCartToggle.addEventListener('click', toggleCart);
}

// Add click outside handler
document.removeEventListener('click', handleOutsideClick);
document.addEventListener('click', handleOutsideClick);

cartInitialized = true;
console.log('Cart initialization complete');
}

function handleOutsideClick(event) {
const cartSlideOut = document.querySelector('.cart-slide-out');
const cartToggle = document.querySelector('.cart-toggle');

if (cartSlideOut && cartSlideOut.classList.contains('active')) {
if (!cartSlideOut.contains(event.target) && !cartToggle.contains(event.target)) {
toggleCart();
}
}
}

// Initialize cart when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Initializing cart…');
initializeCart();
});

// Step Navigation
function goToStep(stepNumber) {
console.log('Navigating to step:', stepNumber);

// Validate step number
if (stepNumber < 1 || stepNumber > 3) {
console.warn('Invalid step number:', stepNumber);
return;
}

// If skipping step 2
if (window.skipAddOnsStep) {
if (stepNumber === 2) stepNumber = 3;
if (stepNumber === 0) stepNumber = 1;
}

// Validate ticket selection when moving to step 2 or 3
if (stepNumber === 2 || stepNumber === 3) {
let totalTickets = 0;
const quantityInputs = document.querySelectorAll('.quantity-value');

quantityInputs.forEach(input => {
totalTickets += parseInt(input.value) || 0;
});

if (totalTickets === 0) {
showSpeechBubble('Please select at least one ticket before proceeding');
return;
}
}

// Hide all steps first
const steps = document.querySelectorAll('.step_1, .step_2, .step_3');
steps.forEach(step => {
step.classList.remove('active');
console.log('Hiding step:', step.className);
});

// Show selected step
const selectedStep = document.querySelector(`.step_${stepNumber}`);
if (selectedStep) {
// Force a reflow to ensure the transition works
selectedStep.offsetHeight;
selectedStep.classList.add('active');
console.log('Showing step:', selectedStep.className);

// Initialize Stripe when reaching Step 3
if (stepNumber === 3) {
console.log('Reached Step 3, initializing Stripe…');
initializeStripe();
}
} else {
console.error(`Step ${stepNumber} element not found`);
return;
}

// Update step indicator
const stepIndicators = document.querySelectorAll('.step-indicator');
stepIndicators.forEach(indicator => {
indicator.textContent = `Step ${stepNumber} of 3`;
});

// Update navigation buttons
updateNavigationButtons(stepNumber);

// Scroll to top of the form
const form = document.getElementById('event-order-form');
if (form) {
form.scrollIntoView({ behavior: 'smooth' });
}
}

function updateNavigationButtons(currentStep) {
console.log('Updating navigation buttons for step:', currentStep);

// Update sticky navigation
const stickyPrev = document.querySelector('.sticky-cart-bar .prev-step');
const stickyNext = document.querySelector('.sticky-cart-bar .next-step');
const buyNowBtn = document.querySelector('.buy-now-btn');

if (stickyPrev) {
stickyPrev.style.display = 'block';
if (currentStep === 1) {
stickyPrev.innerHTML = '';
stickyPrev.onclick = function(event) {
event.preventDefault();
window.location.href = '/index.html';
};
} else {
stickyPrev.innerHTML = '';
stickyPrev.onclick = function(event) {
event.preventDefault();
goToStep(currentStep – 1);
};
}
}

if (stickyNext) {
if (currentStep === 3) {
// Hide the next button in sticky nav on step 3 but maintain space
stickyNext.style.visibility = 'hidden';
stickyNext.classList.add('hide-on-step3');
} else {
stickyNext.style.visibility = 'visible';
stickyNext.classList.remove('hide-on-step3');
stickyNext.innerHTML = '';
stickyNext.classList.remove('complete-purchase');
stickyNext.onclick = function(event) {
event.preventDefault();
goToStep(currentStep + 1);
};
}
}

// Show/hide buy now button
if (buyNowBtn) {
if (currentStep === 3) {
buyNowBtn.style.display = 'block';
buyNowBtn.onclick = function(event) {
event.preventDefault();
handleFormSubmit(event);
};
} else {
buyNowBtn.style.display = 'none';
}
}
}

// Initialize step navigation when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Initializing step navigation…');

// Ensure Step 1 is active by default
const step1 = document.querySelector('.step_1');
const step2 = document.querySelector('.step_2');
const step3 = document.querySelector('.step_3');

if (step1) {
step1.classList.add('active');
console.log('Initialized Step 1 as active');
}

if (step2) {
step2.classList.remove('active');
console.log('Initialized Step 2 as hidden');
}

if (step3) {
step3.classList.remove('active');
console.log('Initialized Step 3 as hidden');
}

// Add click handlers to step navigation buttons
const nextButtons = document.querySelectorAll('.next-step');
const prevButtons = document.querySelectorAll('.prev-step');

nextButtons.forEach(button => {
button.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation(); // Prevent event bubbling

// Find the current active step
const activeStep = document.querySelector('.step_1.active, .step_2.active, .step_3.active');
if (!activeStep) {
console.error('No active step found');
return;
}

const currentStepNumber = parseInt(activeStep.className.match(/step_(d+)/)?.[1] || '1');
console.log('Current step:', currentStepNumber);

// Validate ticket selection before proceeding
if (currentStepNumber === 1) {
let totalTickets = 0;
const quantityInputs = document.querySelectorAll('.quantity-value');

quantityInputs.forEach(input => {
totalTickets += parseInt(input.value) || 0;
});

if (totalTickets === 0) {
showSpeechBubble('Please select at least one ticket before proceeding');
return;
}
}

if (currentStepNumber < 3) {
// Add a small delay to prevent multiple transitions
setTimeout(() => {
goToStep(currentStepNumber + 1);
}, 100);
}
});
});

prevButtons.forEach(button => {
button.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation(); // Prevent event bubbling

// Find the current active step
const activeStep = document.querySelector('.step_1.active, .step_2.active, .step_3.active');
if (!activeStep) {
console.error('No active step found');
return;
}

const currentStepNumber = parseInt(activeStep.className.match(/step_(d+)/)?.[1] || '1');
console.log('Current step:', currentStepNumber);

if (currentStepNumber > 1) {
// If skipping step 2, go directly to step 1 from step 3
let targetStep = currentStepNumber – 1;
if (window.skipAddOnsStep && currentStepNumber === 3) {
targetStep = 1;
}
setTimeout(() => {
goToStep(targetStep);
}, 100);
}
});
});

// Initialize navigation buttons for the first step
updateNavigationButtons(1);
});

// Stripe Integration
let stripe;
let elements;
let stripeInitialized = false;

async function initializeStripe() {
if (stripeInitialized) {
console.log('Stripe already initialized');
return;
}

console.log('Initializing Stripe with Express Checkout…');
try {
// Initialize Stripe with your publishable key
stripe = Stripe('pk_live_KSh8MRi76Kliuk4fUpBZ23GS', {
apiVersion: '2023-10-16',
stripeAccount: 'acct_1C7mK9IQtQgABICR'
});

// Create elements with appearance configuration
elements = stripe.elements({
appearance: {
theme: 'stripe',
variables: {
colorPrimary: '#ed1e26',
colorBackground: '#ffffff',
colorText: '#30313d',
colorDanger: '#df1b41',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
spacingUnit: '4px',
borderRadius: '10px'
}
}
});

// Add custom CSS for larger payment buttons
const style = document.createElement('style');
style.textContent = `
.GooglePayButton {
border: 0 !important;
cursor: pointer !important;
max-height: 84px !important;
min-height: 52px !important;
position: relative !important;
}

.ApplePayButton {
border: 0 !important;
cursor: pointer !important;
max-height: 84px !important;
min-height: 52px !important;
position: relative !important;
}

#payment-request-button {
max-height: 84px !important;
min-height: 52px !important;
}

#payment-request-button button {
max-height: 84px !important;
min-height: 52px !important;
transform: scale(1.2) !important;
transform-origin: center !important;
}
`;
document.head.appendChild(style);

// Create and mount card elements with improved styling
const cardNumber = elements.create('cardNumber', {
placeholder: 'Card number',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card number element is properly mounted
const cardNumberContainer = document.getElementById('card-number');
if (cardNumberContainer) {
// Clear any existing content
cardNumberContainer.innerHTML = '';
cardNumber.mount('#card-number');
console.log('Card number element mounted successfully');
} else {
console.error('Card number container not found');
}

const cardExpiry = elements.create('cardExpiry', {
placeholder: 'MM/YY',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card expiry element is properly mounted
const cardExpiryContainer = document.getElementById('card-expiry');
if (cardExpiryContainer) {
// Clear any existing content
cardExpiryContainer.innerHTML = '';
cardExpiry.mount('#card-expiry');
console.log('Card expiry element mounted successfully');
} else {
console.error('Card expiry container not found');
}

const cardCvc = elements.create('cardCvc', {
placeholder: 'CVC',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card CVC element is properly mounted
const cardCvcContainer = document.getElementById('card-cvc');
if (cardCvcContainer) {
// Clear any existing content
cardCvcContainer.innerHTML = '';
cardCvc.mount('#card-cvc');
console.log('Card CVC element mounted successfully');
} else {
console.error('Card CVC container not found');
}

// Add event listeners to card elements for debugging
cardNumber.on('ready', () => {
console.log('Card number element ready');
// Fix accessibility warnings for hidden Stripe inputs
fixStripeAccessibilityWarnings();
});

cardNumber.on('focus', () => {
console.log('Card number focused');
});

cardNumber.on('blur', () => {
console.log('Card number blurred');
});

// Function to fix Stripe accessibility warnings
function fixStripeAccessibilityWarnings() {
// Remove aria-hidden from Stripe's hidden inputs that are focused
const hiddenInputs = document.querySelectorAll('.__PrivateStripeElement-input[aria-hidden="true"]');
hiddenInputs.forEach(input => {
// Remove aria-hidden attribute to prevent accessibility warnings
input.removeAttribute('aria-hidden');
// Ensure they're properly hidden with CSS instead
input.style.position = 'absolute';
input.style.left = '-9999px';
input.style.top = '-9999px';
input.style.width = '1px';
input.style.height = '1px';
input.style.opacity = '0';
input.style.pointerEvents = 'none';
// Prevent focus
input.setAttribute('tabindex', '-1');
input.style.pointerEvents = 'none';
});
}

// Set up MutationObserver to catch dynamically created Stripe elements
function setupStripeAccessibilityObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if the added node is a Stripe hidden input
if (node.classList && node.classList.contains('__PrivateStripeElement-input')) {
if (node.getAttribute('aria-hidden') === 'true') {
node.removeAttribute('aria-hidden');
node.style.position = 'absolute';
node.style.left = '-9999px';
node.style.top = '-9999px';
node.style.width = '1px';
node.style.height = '1px';
node.style.opacity = '0';
node.style.pointerEvents = 'none';
}
}
// Also check child elements
const hiddenInputs = node.querySelectorAll && node.querySelectorAll('.__PrivateStripeElement-input[aria-hidden="true"]');
if (hiddenInputs) {
hiddenInputs.forEach(input => {
input.removeAttribute('aria-hidden');
input.style.position = 'absolute';
input.style.left = '-9999px';
input.style.top = '-9999px';
input.style.width = '1px';
input.style.height = '1px';
input.style.opacity = '0';
input.style.pointerEvents = 'none';
});
}
}
});
}
});
});

// Start observing the document body for changes
observer.observe(document.body, {
childList: true,
subtree: true
});

return observer;
}

// — Stripe Express Checkout Element (New Implementation) —
// Calculate the total for Express Checkout
function getTotalAmountForExpressCheckout() {
let subtotal = 0;
orderData.tickets.forEach(ticket => {
if (ticket.quantity > 0) {
subtotal += ticket.quantity * ticket.price;
}
});
let addOnsTotal = 0;
orderData.addOns.forEach(addon => {
addOnsTotal += addon.quantity * addon.price;
});
let totalAmount = subtotal + addOnsTotal;

// Apply voucher discount if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (discountMatch) {
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];
if (discountType === '%') {
totalAmount = subtotal * (1 – discountValue / 100) + addOnsTotal;
} else {
totalAmount = Math.max(0, subtotal – discountValue) + addOnsTotal;
}
}
}
return Math.round(totalAmount * 100); // in cents
}

// Create Payment Request Button (legacy API – works without clientSecret)
const paymentRequestAmount = getTotalAmountForExpressCheckout();
console.log('Creating Payment Request Button with amount:', paymentRequestAmount);

// Create payment request
const paymentRequest = stripe.paymentRequest({
country: 'IE',
currency: 'eur',
total: {
label: 'Fossetts Circus Tickets',
amount: paymentRequestAmount,
},
requestPayerName: true,
requestPayerEmail: true,
requestPayerPhone: true,
});

// Create payment request button
let paymentRequestButton;
try {
console.log('Creating payment request button…');
paymentRequestButton = elements.create('paymentRequestButton', {
paymentRequest: paymentRequest,
style: {
paymentRequestButton: {
type: 'default',
theme: 'dark',
height: '44px'
}
}
});
console.log('Payment Request Button created successfully:', paymentRequestButton);
} catch (error) {
console.error('Error creating Payment Request Button:', error);
console.error('Error details:', {
message: error.message,
stack: error.stack,
name: error.name
});
return;
}

// Check if payment request is supported
paymentRequest.canMakePayment().then(function(result) {
console.log('Payment Request canMakePayment result:', result);
// TEMPORARY: Always hide Google Pay and Apple Pay button for debugging
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
return; // Prevent any further logic from showing the button
// — original logic below (commented out) —
/*
if (result) {
console.log('Payment Request is supported, mounting button…');
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer && paymentRequestAmount > 0) {
paymentRequestContainer.innerHTML = '';
paymentRequestContainer.style.display = 'block';
try {
paymentRequestButton.mount('#payment-request-button');
console.log('Payment Request Button mounted successfully');
} catch (error) {
console.error('Error mounting Payment Request Button:', error);
}
} else {
console.log('Payment Request container not found or amount is 0');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
}
} else {
console.log('Payment Request is not supported');
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
}
*/
});

// Handle payment request events
paymentRequest.on('paymentmethod', async function(ev) {
console.log('Payment Request paymentmethod event:', ev);

try {
// Prepare form data
const form = document.getElementById('event-order-form');
const formData = new FormData(form);

// Add wheelchair user count
const wheelchairUsers = getWheelchairUserData();
if (wheelchairUsers > 0) {
formData.append('wheelchair_users', wheelchairUsers);
}

// Add payment token (backend expects payment_token)
formData.append('payment_token', ev.paymentMethod.id);

// Add event and date ids from URL parameters
const urlParams = getUrlParams();
const eventId = urlParams.event_id;
const eventDateId = urlParams.event_date_id;

// Add event and date ids to form data
formData.append('event_id', eventId);
formData.append('event_date_id', eventDateId);

// Add customer information from Payment Request
formData.append('customer_name', ev.payerName || '');
formData.append('customer_email', ev.payerEmail || '');
formData.append('customer_confirm_email', ev.payerEmail || '');
formData.append('customer_phone', ev.payerPhone || '');

// Collect ticket quantities (same as regular form submission)
console.log('Collecting ticket quantities for Payment Request…');
const sectionQuantities = {};
const quantityInputs = document.querySelectorAll('.quantity-value[data-section][data-ticket-type]');

quantityInputs.forEach(input => {
const section = input.dataset.section;
const ticketType = input.dataset.ticketType;
const quantity = parseInt(input.value) || 0;

if (!sectionQuantities[section]) {
sectionQuantities[section] = {};
}

if (ticketType && quantity > 0) {
sectionQuantities[section][ticketType.toLowerCase()] = quantity;
}

console.log('Processing ticket input:', {
section,
ticketType,
quantity,
value: input.value
});
});

console.log('Section quantities:', sectionQuantities);

// Send all ticket types dynamically with seating area information
Object.entries(sectionQuantities).forEach(([section, sectionData]) => {
Object.entries(sectionData).forEach(([ticketType, quantity]) => {
// Convert spaces to underscores in the ticket type name for the form field
// The server expects: event_seat_{seating_area}_{ticket_type_with_underscores}
const cleanTicketType = ticketType.replace(/s+/g, '_');
formData.append(`event_seat_${section}_${cleanTicketType}`, quantity);

console.log('Added form field:', `event_seat_${section}_${cleanTicketType}`, '=', quantity);
console.log('Ticket type details:', {
section: section,
originalTicketType: ticketType,
cleanTicketType: cleanTicketType,
quantity: quantity,
fieldName: `event_seat_${section}_${cleanTicketType}`
});
});
});

// Add add-ons
console.log('Collecting add-ons for Payment Request…');
const addonInputs = document.querySelectorAll('.addon-list-item .quantity-value');
console.log('Found addon inputs:', addonInputs.length);

addonInputs.forEach(input => {
const addonId = input.dataset.addonId;
const addonName = input.dataset.addonName;
const quantity = parseInt(input.value) || 0;

console.log('Processing addon input:', {
addonId,
addonName,
quantity,
value: input.value
});

if (addonId && quantity > 0) {
formData.append(`addon_${addonId}`, quantity);
console.log(`Added addon_${addonId} = ${quantity}`);
}
});

// Add voucher if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const voucherMatch = voucherText.match(/Voucher: ([A-Z0-9]+)/);
if (voucherMatch) {
formData.append('voucher_code', voucherMatch[1]);
}
}

// Submit order to server
const response = await fetch(`/order.php?event_id=${eventId}&event_date_id=${eventDateId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});

const result = await response.json();
if (result.success) {
// Complete the payment request successfully
ev.complete('success');
window.location.href = result.redirect_url;
} else {
// Complete the payment request with error
ev.complete('fail');
showSpeechBubble(result.message || 'Payment failed. Please try again.');
}
} catch (error) {
console.error('Error handling Payment Request payment:', error);
// Complete the payment request with error
ev.complete('fail');
showSpeechBubble('Payment processing failed. Please try again.');
}
});

paymentRequest.on('cancel', function() {
console.log('Payment Request cancelled');
});

// Update Payment Request amount when order changes
function updatePaymentRequestAmount() {
const newAmount = getTotalAmountForExpressCheckout();
paymentRequest.update({
total: {
label: 'Fossetts Circus Tickets',
amount: newAmount,
}
});
}

// Store the update function globally for use in order updates
window.updatePaymentRequestAmount = updatePaymentRequestAmount;

stripeInitialized = true;
console.log('Stripe Payment Request Button initialized successfully');

// Set up observer to catch dynamically created Stripe elements
const stripeObserver = setupStripeAccessibilityObserver();

// Suppress specific Stripe accessibility warnings
const originalWarn = console.warn;
console.warn = function(…args) {
const message = args.join(' ');
if (message.includes('aria-hidden') && message.includes('Stripe')) {
// Suppress Stripe aria-hidden warnings
return;
}
originalWarn.apply(console, args);
};

// Fix accessibility warnings after all elements are mounted
setTimeout(() => {
fixStripeAccessibilityWarnings();
}, 1000);

} catch (error) {
console.error('Error initializing Stripe Express Checkout:', error);
throw error;
}
}

// Initialize payment functionality when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Initializing payment…');

// Initialize Stripe
initializeStripe();

// Add form submit handler
const form = document.getElementById('event-order-form');
if (form) {
form.addEventListener('submit', handleFormSubmit);
}

// Add click handler for complete purchase button
const completePurchaseBtn = document.querySelector('.next-step.complete-purchase');
if (completePurchaseBtn) {
completePurchaseBtn.addEventListener('click', function(event) {
event.preventDefault();
handleFormSubmit(event);
});
}
});

async function handleFormSubmit(event) {
event.preventDefault();
console.log('Form submission started');

// Ensure order summary is up to date before checking total
updateOrderSummary();

// Recalculate total directly before checking minimum
let subtotal = 0;
orderData.tickets.forEach(ticket => {
if (ticket.quantity > 0) {
subtotal += ticket.quantity * ticket.price;
}
});
let addOnsTotal = 0;
orderData.addOns.forEach(addon => {
addOnsTotal += addon.quantity * addon.price;
});
let totalAmount = subtotal + addOnsTotal;

// Apply voucher discount if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (discountMatch) {
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];
if (discountType === '%') {
totalAmount = subtotal * (1 – discountValue / 100) + addOnsTotal;
} else {
totalAmount = Math.max(0, subtotal – discountValue) + addOnsTotal;
}
}
}

// Log detailed payment calculation
console.log('Payment calculation details:', {
ticketSubtotal: subtotal,
addOnsTotal: addOnsTotal,
totalBeforeVoucher: subtotal + addOnsTotal,
totalAfterVoucher: totalAmount,
ticketDetails: orderData.tickets.map(t => ({
type: t.type,
quantity: t.quantity,
price: t.price,
subtotal: t.quantity * t.price
})),
addonDetails: orderData.addOns.map(a => ({
name: a.name,
quantity: a.quantity,
price: a.price,
subtotal: a.quantity * a.price
}))
});

// Validate minimum payment amount
if (totalAmount < 0.5) {
const errorMessage = `The minimum payment amount is €0.50. Current total: €${totalAmount.toFixed(2)}`;
console.error(errorMessage);
showSpeechBubble(errorMessage);
return;
}

// Validate form
const form = document.getElementById('event-order-form');
if (!form) {
console.error('Form not found');
return;
}

// Get form elements safely
const customerName = form.querySelector('[name="customer_name"]')?.value?.trim();
const customerPhone = form.querySelector('[name="customer_phone"]')?.value?.trim();
const email = document.getElementById('email_address')?.value?.trim();
const confirmEmail = document.getElementById('confirm_email_address')?.value?.trim();

// Validate required fields
if (!customerName) {
showSpeechBubble('Please enter your full name');
return;
}
if (!customerPhone) {
showSpeechBubble('Please enter your phone number');
return;
}
if (!email) {
showSpeechBubble('Please enter your email address');
return;
}
if (!confirmEmail) {
showSpeechBubble('Please confirm your email address');
return;
}

// Validate email match
if (email !== confirmEmail) {
showSpeechBubble('Email addresses do not match');
return;
}

// Validate email format
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
if (!emailRegex.test(email)) {
showSpeechBubble('Please enter a valid email address');
return;
}

// Validate phone number (basic validation)
const phoneRegex = /^[ds+()-]{8,}$/;
if (!phoneRegex.test(customerPhone)) {
showSpeechBubble('Please enter a valid phone number');
return;
}

// Validate card details
if (!stripe || !elements) {
showSpeechBubble('Payment system not initialized. Please refresh the page.');
return;
}

const cardNumber = elements.getElement('cardNumber');
const cardExpiry = elements.getElement('cardExpiry');
const cardCvc = elements.getElement('cardCvc');

if (!cardNumber || !cardExpiry || !cardCvc) {
showSpeechBubble('Please enter all card details');
return;
}

// Get submit button (check both locations)
const submitButton = document.querySelector('.buy-now-btn') || document.querySelector('.next-step.complete-purchase');
if (!submitButton) {
console.error('Submit button not found');
showSpeechBubble('Error: Submit button not found. Please refresh the page.');
return;
}

// Disable submit button
submitButton.disabled = true;
submitButton.textContent = 'Processing…';

try {
// Get event IDs from URL
const urlParams = new URLSearchParams(window.location.search);
const eventId = urlParams.get('event_id');
const eventDateId = urlParams.get('event_date_id');

if (!eventId || !eventDateId) {
throw new Error('Missing event information');
}

// Create FormData object
const formData = new FormData();

// Add form data
formData.append('customer_name', customerName);
formData.append('customer_email', email);
formData.append('customer_confirm_email', confirmEmail);
formData.append('customer_phone', customerPhone);
formData.append('event_id', eventId);
formData.append('event_date_id', eventDateId);

// Sum ticket quantities across all sections – DYNAMIC APPROACH
// Instead of hardcoded mappings, we'll send all ticket types as they are

// Debug log for ticket collection
console.log('Collecting ticket quantities…');

// Collect all quantities by section and type
const sectionQuantities = {};
const quantityInputs = document.querySelectorAll('.quantity-value[data-section][data-ticket-type]');

quantityInputs.forEach(input => {
const section = input.dataset.section;
const ticketType = input.dataset.ticketType;
const quantity = parseInt(input.value) || 0;

if (!sectionQuantities[section]) {
sectionQuantities[section] = {};
}

if (ticketType && quantity > 0) {
// Store the original ticket type name as it appears in the database
sectionQuantities[section][ticketType] = quantity;
}

console.log('Processing ticket input:', {
section,
ticketType,
quantity,
value: input.value
});
});

// Log the collected ticket counts
console.log('Section quantities:', sectionQuantities);

// Send all ticket types dynamically with seating area information
Object.entries(sectionQuantities).forEach(([section, sectionData]) => {
Object.entries(sectionData).forEach(([ticketType, quantity]) => {
// Convert spaces to underscores in the ticket type name for the form field
// The server expects: event_seat_{seating_area}_{ticket_type_with_underscores}
const cleanTicketType = ticketType.replace(/s+/g, '_');
formData.append(`event_seat_${section}_${cleanTicketType}`, quantity);

console.log('Added form field:', `event_seat_${section}_${cleanTicketType}`, '=', quantity);
console.log('Ticket type details:', {
section: section,
originalTicketType: ticketType,
cleanTicketType: cleanTicketType,
quantity: quantity,
fieldName: `event_seat_${section}_${cleanTicketType}`
});
});
});

// Add add-ons
console.log('Collecting add-ons…');
const addonInputs = document.querySelectorAll('.addon-list-item .quantity-value');
console.log('Found addon inputs:', addonInputs.length);

addonInputs.forEach(input => {
const addonId = input.dataset.addonId;
const addonName = input.dataset.addonName;
const quantity = parseInt(input.value) || 0;

console.log('Processing addon input:', {
addonId,
addonName,
quantity,
value: input.value,
dataset: input.dataset
});

if (addonId && quantity > 0) {
formData.append(`addon_${addonId}`, quantity);
console.log(`Added addon_${addonId} = ${quantity}`);
}
});

// Add voucher if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherInput = document.querySelector('input[name="voucher"]');
if (voucherInput) {
formData.append('voucher', voucherInput.value.trim());
}
}

// Add wheelchair user count
const wheelchairUsers = getWheelchairUserData();
if (wheelchairUsers > 0) {
formData.append('wheelchair_users', wheelchairUsers);
}

console.log('Submitting form data:', {
eventId,
eventDateId,
customerName,
email,
customerPhone,
formData: Object.fromEntries(formData)
});

// Debug: Log all form data entries
console.log('All form data entries:');
for (let [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}

// Create payment method using existing card elements
const { error: paymentMethodError, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardNumber,
billing_details: {
name: customerName,
email: email,
phone: customerPhone
}
});

if (paymentMethodError) {
throw new Error(paymentMethodError.message);
}

// Add payment token to form data
formData.append('payment_token', paymentMethod.id);

// Submit order to server
const response = await fetch(`/order.php?event_id=${eventId}&event_date_id=${eventDateId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});

console.log('Server response status:', response.status);
const responseText = await response.text();
console.log('Server response text:', responseText);

let result;
try {
result = JSON.parse(responseText);
console.log('Parsed server response:', result);

// Log detailed error information if present
if (!result.success) {
console.error('Server error details:', {
message: result.message,
status: result.status,
total: totalAmount,
formData: Object.fromEntries(formData)
});
}
} catch (e) {
console.error('Failed to parse server response:', e);
console.error('Raw response:', responseText);
throw new Error('Server returned an invalid response');
}

if (!result) {
throw new Error('Empty response from server');
}

if (result.error) {
throw new Error(result.error);
}

if (result.requires_action) {
// Handle 3D Secure authentication
const { error: confirmError } = await stripe.confirmCardPayment(
result.payment_intent_client_secret
);

if (confirmError) {
throw new Error(confirmError.message);
}

// Retry the order submission with the payment intent ID
formData.append('payment_intent_id', result.payment_intent_id);
const finalResponse = await fetch(`/order.php?event_id=${eventId}&event_date_id=${eventDateId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});

if (!finalResponse.ok) {
throw new Error(`Server error: ${finalResponse.status}`);
}

const finalResult = await finalResponse.json();
if (!finalResult.success) {
throw new Error(finalResult.message || 'Order processing failed');
}

// Redirect directly to the detailed success page
if (finalResult.redirect_url) {
window.location.href = finalResult.redirect_url;
} else {
// Fallback if no redirect URL
const successMessage = document.createElement('div');
successMessage.className = 'success-message';
successMessage.innerHTML = `
Order Successful!
Thank you for your purchase. Your order has been processed successfully.
Order Reference: ${finalResult.order_code || 'N/A'}
A confirmation email has been sent to ${email}

Return to Home

`;

// Replace form content with success message
const form = document.getElementById('event-order-form');
if (form) {
form.innerHTML = '';
form.appendChild(successMessage);
}
}
} else if (result.success) {
// Redirect directly to the detailed success page
if (result.redirect_url) {
window.location.href = result.redirect_url;
} else {
// Fallback if no redirect URL
const successMessage = document.createElement('div');
successMessage.className = 'success-message';
successMessage.innerHTML = `
Order Successful!
Thank you for your purchase. Your order has been processed successfully.
Order Reference: ${result.order_code || 'N/A'}
A confirmation email has been sent to ${email}

Return to Home

`;

// Replace form content with success message
const form = document.getElementById('event-order-form');
if (form) {
form.innerHTML = '';
form.appendChild(successMessage);
}
}
} else {
throw new Error(result.message || 'Order processing failed');
}

} catch (error) {
console.error('Payment error:', error);
showSpeechBubble(error.message || 'An error occurred during payment processing');
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = 'Complete Purchase';
}
}
}

function updateCartDisplay(ticketData, subtotal, addOnsTotal, total, shouldSave = true) {
console.log('updateCartDisplay called with:', { ticketData, subtotal, addOnsTotal, total, shouldSave });
console.log('orderData.addOns:', orderData.addOns);

// Generate cart items HTML
let cartItemsHTML = '';

// Add ticket items
if (Array.isArray(ticketData)) {
ticketData.forEach(ticket => {
// Format the section name (remove underscores and capitalize)
const sectionName = ticket.section
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');

// Dynamic ticket type mapping – handle any ticket type
let typeLabel = ticket.type;

// Only apply mapping for legacy ticket types, keep new types as-is
const legacyTypeMapping = {
'FULL': 'Adult',
'CHILD': 'Child',
'SENIOR': 'Senior'
};

if (legacyTypeMapping[ticket.type]) {
typeLabel = legacyTypeMapping[ticket.type];
}

cartItemsHTML += `

${typeLabel} – ${sectionName}
€${ticket.price.toFixed(2)} each remove

x${ticket.quantity}
€${(ticket.price * ticket.quantity).toFixed(2)}

`;
});
}

// Add add-ons
if (Array.isArray(orderData.addOns)) {
console.log('Processing add-ons in updateCartDisplay:', orderData.addOns);
orderData.addOns.forEach(addon => {
if (addon.quantity > 0) {
const displayName = addon.label || addon.name;
console.log('Addon display name:', displayName, 'for addon:', addon);
cartItemsHTML += `

${displayName}
€${addon.price.toFixed(2)} each remove

x${addon.quantity}
€${(addon.price * addon.quantity).toFixed(2)}

`;
}
});
}

// Add voucher discount if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const voucherMatch = voucherText.match(/Voucher Applied: (w+)/);
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (voucherMatch && discountMatch) {
const voucherCode = voucherMatch[1];
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];
let discountAmount = 0;

if (discountType === '%') {
// Apply discount only to ticket subtotal
discountAmount = subtotal * (discountValue / 100);
} else {
// For fixed amount, apply to ticket total only
discountAmount = Math.min(discountValue, subtotal);
}

cartItemsHTML += `

Voucher Applied: ${voucherCode}
Discount on tickets -${discountValue}${discountType} (remove)

-€${discountAmount.toFixed(2)}

`;
}
}

// Update all cart items containers
const cartItemsContainers = document.querySelectorAll('.cart-items');
cartItemsContainers.forEach(container => {
if (container) {
container.innerHTML = cartItemsHTML;
}
});

// Update the main order summary total
const summaryTotal = document.getElementById('summary-total-amount');
if (summaryTotal) {
summaryTotal.textContent = `€${total.toFixed(2)}`;
}

// Update the sticky cart bar total
const stickyCartTotal = document.querySelector('.sticky-cart-bar .cart-total-amount');
if (stickyCartTotal) {
stickyCartTotal.textContent = `€${total.toFixed(2)}`;
}

// Add click handlers for remove buttons
document.querySelectorAll('.remove-item').forEach(button => {
button.addEventListener('click', function() {
const type = this.dataset.type;
console.log('Remove button clicked:', { type, dataset: this.dataset });

if (type === 'ticket') {
const section = this.dataset.section;
const ticketType = this.dataset.ticketType;

console.log('Attempting to remove ticket:', { section, ticketType });

// Try to find the quantity input with the exact ticket type
let quantityInput = document.querySelector(`.quantity-value[data-section="${section}"][data-ticket-type="${ticketType}"]`);

// If not found, try case-insensitive search
if (!quantityInput) {
const allQuantityInputs = document.querySelectorAll('.quantity-value[data-section="' + section + '"]');
console.log('All quantity inputs for section', section, ':', allQuantityInputs);

// Try to find by exact match first, then case-insensitive
for (let input of allQuantityInputs) {
const inputTicketType = input.dataset.ticketType;
if (inputTicketType === ticketType ||
(inputTicketType && inputTicketType.toLowerCase() === ticketType.toLowerCase())) {
quantityInput = input;
console.log('Found quantity input:', inputTicketType);
break;
}
}
}

if (quantityInput) {
console.log('Found quantity input, setting to 0:', quantityInput);
quantityInput.value = '0';
updateTicketQuantity(section, ticketType, parseFloat(quantityInput.dataset.price), 0);
} else {
console.error('Could not find quantity input for ticket removal:', { section, ticketType });

// Fallback: manually remove from orderData
const ticketIndex = orderData.tickets.findIndex(t =>
t.section === section &&
(t.type === ticketType || t.type.toLowerCase() === ticketType.toLowerCase())
);

if (ticketIndex !== -1) {
console.log('Removing ticket from orderData directly:', orderData.tickets[ticketIndex]);
orderData.tickets.splice(ticketIndex, 1);
updateOrderSummary();
} else {
console.error('Could not find ticket in orderData to remove');
// Force refresh the cart display
updateOrderSummary();
}
}
} else if (type === 'addon') {
const name = this.dataset.name;
const quantityInput = document.querySelector(`.add-on-item .quantity-value[data-addon-name="${name}"]`);
if (quantityInput) {
quantityInput.value = '0';
updateAddonQuantity(name, parseFloat(quantityInput.dataset.price), 0);
}
} else if (type === 'voucher') {
// Clear voucher
const voucherInput = document.querySelector('input[name="voucher"]');
const warningVoucher = document.querySelector('.warning-voucher');
const successVoucher = document.querySelector('.success-voucher');
if (voucherInput) voucherInput.value = '';
if (warningVoucher) warningVoucher.style.display = 'none';
if (successVoucher) successVoucher.style.display = 'none';
updateOrderSummary();
}
});
});

// Only save order data if shouldSave is true
if (shouldSave) {
saveOrderData({
tickets: ticketData || [],
addOns: orderData.addOns || [],
total: total
});
}
}

function addRemoveListenersToSummary() {
document.querySelectorAll('.remove-item').forEach(el => {
el.addEventListener('click', function() {
const type = this.getAttribute('data-type');
const name = this.getAttribute('data-name');
if (type === 'addon') {
updateAddonQuantity(name, 0, 0);
// Update the UI input if present
const input = document.querySelector(`.quantity-value[data-addon-name="${name}"]`);
if (input) input.value = 0;
} else if (type === 'ticket') {
const section = this.getAttribute('data-section');
const ticketType = this.getAttribute('data-ticket-type');
const input = document.querySelector(`.quantity-value[data-section="${section}"][data-ticket-type="${ticketType.toLowerCase()}"]`);
if (input) {
input.value = 0;
input.dispatchEvent(new Event('change', { bubbles: true }));
}
}
});
});
}

function getUrlParams() {
const params = new URLSearchParams(window.location.search);
return {
event_id: params.get('event_id'),
event_date_id: params.get('event_date_id')
};
}

async function fetchEventData(event_id, event_date_id) {
console.log('Starting fetchEventData…');
console.log('URL parameters:', { event_id, event_date_id });

try {
const url = `/api/events.php?event_id=${event_id}&event_date_id=${event_date_id}`;
console.log('Making request to:', url);

const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
cache: 'no-cache'
});

console.log('Response status:', response.status);

if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}

const data = await response.json();
console.log('API response:', data);

if (data.error) {
throw new Error(data.error);
}

// Find the specific event date
const eventDate = data.event_dates.find(date => date.event_date_id === parseInt(event_date_id));
console.log('Found event date:', eventDate);

if (!eventDate) {
throw new Error('Event date not found');
}

// Check if showtime is active
if (eventDate.showtime_status !== 'active') {
throw new Error('This showtime is not available for booking');
}

// Check if booking is closed (within 30 minutes of showtime)
if (eventDate.booking_closed) {
// Calculate minutes until showtime
const showtime = new Date(eventDate.event_datetime);
const now = new Date();
const timeDiff = showtime – now;
const minutesUntilShow = Math.floor(timeDiff / (1000 * 60));

// Show online closed message with countdown
const onlineClosedMessage = `ONLINE CLOSED – Starts in ${minutesUntilShow} min`;

// Update the page to show online closed message
document.body.innerHTML = `

${onlineClosedMessage}

Go to Boxoffice

Return to Home

`;
return; // Stop further processing
}

// Format the date string
const dateString = new Date(eventDate.event_datetime).toLocaleString('en-GB', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
console.log('Formatted date string:', dateString);

// Store event date string globally
selectedEventDateString = dateString;

// Update venue info in DOM
const venueTitle = document.getElementById('venue-title');
const venueFull = document.getElementById('venue-full');
const eventDateElement = document.getElementById('event-date');
const summaryEventDate = document.getElementById('summary-event-date');

if (venueTitle) venueTitle.textContent = data.event.venue_title;
if (venueFull) venueFull.textContent = data.event.venue_full;
if (eventDateElement) eventDateElement.textContent = dateString;
if (summaryEventDate) summaryEventDate.textContent = dateString;

// Update order info block
const orderInfoBlock = document.querySelector('.order-info-block');
if (orderInfoBlock) {
orderInfoBlock.innerHTML = `
Order Information

${dateString}

${data.event.venue_title}

${data.event.venue_full}

`;
}

// Fetch dynamic add-ons
let addOns = [];
try {
const addonsResponse = await fetch('https://boxoffice.fossettscircus.com/api/addons', {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
cache: 'no-cache'
});

if (addonsResponse.ok) {
const contentType = addonsResponse.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const addonsData = await addonsResponse.json();
addOns = addonsData.map(addon => ({
id: addon.id,
name: addon.name.toLowerCase().replace(/s+/g, ''),
label: addon.name,
description: addon.description || '',
price: parseFloat(addon.price),
img: addon.thumb || '/elements/fossetts-popcorn.jpg'
}));
}
}
} catch (e) {
console.warn('Error fetching add-ons:', e);
}

// Store add-ons data globally for label lookup
globalAddOnsData = addOns;
console.log('Global add-ons data stored:', globalAddOnsData);

// Process the new data structure with capacities and ticket_definitions
const tickets = [];

// Group ticket definitions by seating area
const ticketGroups = {};
eventDate.ticket_definitions.forEach(ticketDef => {
if (!ticketGroups[ticketDef.seating_area]) {
ticketGroups[ticketDef.seating_area] = [];
}
ticketGroups[ticketDef.seating_area].push(ticketDef);
});

// Create ticket sections for each seating area
Object.keys(ticketGroups).forEach(seatingArea => {
const ticketDefs = ticketGroups[seatingArea];
const capacity = eventDate.capacities.find(cap => cap.seating_area === seatingArea);

console.log('Processing seating area:', seatingArea);
console.log('Ticket definitions for this area:', ticketDefs);

tickets.push({
title: seatingArea,
css_class: seatingArea.toLowerCase().replace(/s+/g, '-'),
seating_type: 'Non allocated seating',
capacity: capacity ? capacity.capacity : 0,
ticket_types: ticketDefs.map(ticketDef => {
console.log('Processing ticket definition:', ticketDef);
return {
label: ticketDef.name,
price: parseFloat(ticketDef.price),
select_name: `event_seat_${ticketDef.name.toLowerCase().replace(/s+/g, '_')}`,
seats_per_ticket: ticketDef.seats_per_ticket || 1
};
})
});
});

console.log('Processed tickets:', tickets);

return {
venue_title: data.event.venue_title || '',
venue_full: data.event.venue_full || '',
event_date_string: dateString,
tickets: tickets,
add_ons: addOns
};
} catch (error) {
console.error('Error in fetchEventData:', error);
throw error;
}
}

function createTicketType(ticket) {
const ticketType = document.createElement('div');
ticketType.className = 'ticket-type';
ticketType.innerHTML = `
${ticket.ticket_type}


0
+

`;

// Add event listeners for plus and minus buttons
const minusBtn = ticketType.querySelector('.minus');
const plusBtn = ticketType.querySelector('.plus');
const quantityValue = ticketType.querySelector('.quantity-value');

minusBtn.addEventListener('click', () => {
const currentValue = parseInt(quantityValue.textContent);
if (currentValue > 0) {
quantityValue.textContent = currentValue – 1;
updateTicketQuantity(ticket.ticket_id, currentValue – 1);
}
});

plusBtn.addEventListener('click', () => {
const currentValue = parseInt(quantityValue.textContent);
if (currentValue < ticket.seats_left) {
quantityValue.textContent = currentValue + 1;
updateTicketQuantity(ticket.ticket_id, currentValue + 1);
}
});

return ticketType;
}

function updateTicketQuantity(section, ticketType, price, quantity) {
// Normalize ticket type to uppercase for consistency, but preserve original for display
const normalizedTicketType = ticketType.toUpperCase();

// Find or create ticket in orderData
const ticketIndex = orderData.tickets.findIndex(t =>
t.section === section && t.type === normalizedTicketType
);

if (ticketIndex === -1) {
// Add new ticket
orderData.tickets.push({
section: section,
type: normalizedTicketType,
price: price,
quantity: quantity
});
} else {
// Update existing ticket
orderData.tickets[ticketIndex].quantity = quantity;
}

console.log('Updated ticket quantity:', {
section: section,
ticketType: ticketType,
normalizedType: normalizedTicketType,
price: price,
quantity: quantity,
orderData: orderData.tickets
});

// Update cart display
updateOrderSummary();
}
change in above code as per hosted checkout

Assistant
Great — since you want to switch from the custom Stripe Elements implementation to **Stripe Hosted Checkout**, and your current `order.php` already collects all the order data (tickets, add-ons, voucher, etc.), here's a complete guide to migrate cleanly:

## ✅ Objective

Replace the existing Stripe Elements flow with:

1. **Frontend** sends ticket/order data to backend.
2. **Backend** (`order.php`) creates a **Stripe Checkout Session**.
3. Frontend receives the `sessionId` and redirects user to Stripe.
4. Stripe handles payment and redirects back to your success URL.

## ✅ Step-by-Step Implementation

### 1. 🧠 Decide: Replace or Add?

You can either:
– Replace the current `order.php` logic (if Stripe Elements is only used for payment).
– Or **split** logic:
– `order.php` = form validation and email logic.
– `create_checkout_session.php` = just Stripe.

➡️ **Recommendation**: Modify `order.php` to handle both: form data + session creation.

### 2. ✅ Backend: Modify `order.php` to return `sessionId`

At the **end of your current `order.php`** file (after validating tickets, add-ons, voucher, etc.), add this:

require_once 'vendor/autoload.php';

StripeStripe::setApiKey('sk_live_YOUR_SECRET_KEY');

$YOUR_DOMAIN = 'https://yourdomain.com'; // change to your domain

// Calculate total amount in cents
$totalAmountInCents = round($totalAmount * 100); // Use your existing $totalAmount

// You can customize line items as per your ticket logic
$lineItems = [[
    'price_data' => [
        'currency' => 'eur',
        'product_data' => [
            'name' => 'Fossetts Circus Tickets',
            'description' => 'Order for Event ID: ' . $eventId,
            'metadata' => [
                'event_id' => $eventId,
                'event_date_id' => $eventDateId
            ],
        ],
        'unit_amount' => $totalAmountInCents,
    ],
    'quantity' => 1,
]];

// Optional: Add-ons as additional line items
foreach ($orderData['addOns'] as $addon) {
    if ($addon['quantity'] > 0) {
        $lineItems[] = [
            'price_data' => [
                'currency' => 'eur',
                'product_data' => [
                    'name' => $addon['name'],
                    'description' => 'Add-on',
                ],
                'unit_amount' => round($addon['price'] * 100),
            ],
            'quantity' => $addon['quantity'],
        ];
    }
}

// Create Checkout Session
$session = StripeCheckoutSession::create([
    'payment_method_types' => ['card'],
    'line_items' => $lineItems,
    'mode' => 'payment',
    'success_url' => $YOUR_DOMAIN . '/success.php?session_id={CHECKOUT_SESSION_ID}',
    'cancel_url' => $YOUR_DOMAIN . '/cancel.php',
]);

echo json_encode([
    'success' => true,
    'sessionId' => $session->id
]);
exit;

### 3. ✅ Frontend: Modify your JS `stripe.redirectToCheckout`

Replace your current Stripe `confirmCardPayment` or `Elements` logic with:

document.getElementById('checkout-button').addEventListener('click', async function() {
    const form = document.getElementById('event-order-form');
    const formData = new FormData(form);

    try {
        const response = await fetch('/order.php', {
            method: 'POST',
            body: formData
        });

        const result = await response.json();

        if (result.success && result.sessionId) {
            const stripe = Stripe('pk_live_YOUR_PUBLISHABLE_KEY');
            stripe.redirectToCheckout({ sessionId: result.sessionId });
        } else {
            alert('Checkout failed. Please try again.');
        }
    } catch (err) {
        console.error('Error starting checkout:', err);
        alert('Something went wrong. Please refresh and try again.');
    }
});

> 🔁 You can reuse your existing form, quantities, vouchers — nothing changes in your UI logic.

### 4. ✅ `success.php`: Confirm and show ticket summary

require_once 'vendor/autoload.php';

StripeStripe::setApiKey('sk_live_YOUR_SECRET_KEY');

$sessionId = $_GET['session_id'];
$session = StripeCheckoutSession::retrieve($sessionId);

// Optional: Retrieve full payment and customer details
$paymentIntent = StripePaymentIntent::retrieve($session->payment_intent);

echo "<h2>🎟️ Thank you! Your order is confirmed.</h2>";
echo "<p>Reference: {$session->id}</p>";
echo "<p>Email: {$session->customer_email}</p>";
// You can display more details or generate a QR/ticket now

### 🔐 Security Notes

– Always validate form inputs on server (`order.php`) before session creation.
– Do not trust data from JS — sanitize `event_id`, `quantities`, etc.
– Use Stripe Webhooks for more secure fulfillment (optional).

### ✅ Final Benefits

| Feature | Stripe Elements | Stripe Hosted Checkout |
|————————————|——————|————————–|
| Facebook/Instagram In-App support | ❌ Often broken | ✅ Always works |
| PCI compliance | Medium effort | ✅ Stripe handles it |
| Handles card errors & retries | You handle | ✅ Stripe handles |
| Apple/Google Pay | You set up | ✅ Built-in |
| QR, ticket generation afterward | ✅ Supported | ✅ Supported |

Would you like me to generate the exact replacement `order.php` based on your current code structure (with ticket/add-on logic intact), or do you already have the logic and just need this structure added?

User
apply change in below file
let orderData = {
tickets: [],
addOns: [],
total: 0
};

// Store event date string globally for use in order summary
let selectedEventDateString = '';

// Global add-ons data for label lookup
let globalAddOnsData = [];

// Cache DOM elements
const domCache = {
form: null,
ticketSections: null,
addOnsGrid: null,
cart: null
};

// Initialize DOM cache
function initializeDomCache() {
domCache.form = document.getElementById('event-order-form');
domCache.ticketSections = document.getElementById('ticket-sections');
domCache.addOnsGrid = document.getElementById('add-ons-grid');
domCache.cart = document.getElementById('cart');
}

function updateOrderSummary() {
console.log('updateOrderSummary called');
console.log('Current orderData:', orderData);

if (!domCache.form) return;

// Update event date/time in order summary (step 3)
const summaryEventDate = document.getElementById('summary-event-date');
if (summaryEventDate) {
summaryEventDate.textContent = selectedEventDateString || '';
}

let subtotal = 0;
let ticketData = [];

// Process each ticket
orderData.tickets.forEach(ticket => {
if (ticket.quantity > 0) {
const itemSubtotal = ticket.quantity * ticket.price;
subtotal += itemSubtotal;
ticketData.push({
section: ticket.section,
type: ticket.type,
quantity: ticket.quantity,
price: ticket.price,
subtotal: itemSubtotal
});
}
});

// Calculate add-ons total
const addOnsTotal = calculateAddOns();
let total = subtotal + addOnsTotal;

// Apply voucher discount if valid – only to tickets
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const voucherMatch = voucherText.match(/Voucher Applied: (w+)/);
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (voucherMatch && discountMatch) {
const voucherCode = voucherMatch[1];
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];

if (discountType === '%') {
// Apply discount only to ticket subtotal
const discountedTicketTotal = subtotal * (1 – discountValue / 100);
total = discountedTicketTotal + addOnsTotal;
} else {
// For fixed amount, apply to ticket total only
const discountedTicketTotal = Math.max(0, subtotal – discountValue);
total = discountedTicketTotal + addOnsTotal;
}
}
}

// Update cart display
updateCartDisplay(ticketData, subtotal, addOnsTotal, total);
saveOrderDataToStorage();
addRemoveListenersToSummary();

// Update Express Checkout amount if available
if (window.updatePaymentRequestAmount) {
window.updatePaymentRequestAmount();
}
}

function calculateSubtotal() {
let subtotal = 0;
const ticketSelects = document.querySelectorAll('select[name^="event_seat_"]');

for (const select of ticketSelects) {
subtotal += parseInt(select.value) * parseFloat(select.dataset.price);
}

return subtotal;
}

function calculateAddOns() {
let addOnsTotal = 0;
orderData.addOns.forEach(addon => {
addOnsTotal += addon.quantity * addon.price;
});
return addOnsTotal;
}

function generateOrderSummaryHTML() {
let html = '';

// Add tickets to summary
if (orderData?.tickets?.length > 0) {
html += orderData.tickets
.filter(ticket => ticket?.type && ticket?.price && ticket?.quantity)
.map(ticket => `

${ticket.type} Ticket
€${ticket.price.toFixed(2)} each

x${ticket.quantity}
€${(ticket.price * ticket.quantity).toFixed(2)}

`).join('');
}

// Add wheelchair count to summary
const wheelchairUsers = getWheelchairUserData();
if (wheelchairUsers > 0) {
html += `

Wheelchair
Access required

x${wheelchairUsers}

`;
}

// Add add-ons to summary
if (orderData?.addOns?.length > 0) {
console.log('Generating add-ons summary for:', orderData.addOns);
html += orderData.addOns
.filter(addon => addon?.name && addon?.price && addon?.quantity)
.map(addon => {
console.log('Processing addon:', addon);
const displayName = addon.label || addon.name;
console.log('Display name:', displayName);
return `

${displayName}
€${addon.price.toFixed(2)} each

x${addon.quantity}
€${(addon.price * addon.quantity).toFixed(2)}

`;
}).join('');
}

// If cart is empty, show message
if (!orderData?.tickets?.length && !orderData?.addOns?.length && wheelchairUsers === 0) {
html = `

Your cart is empty
Select tickets to continue

`;
} else {
// Add total if there are items
const total = orderData.total || 0;
html += `

Total:
€${total.toFixed(2)}

`;
}

return html;
}

function saveOrderData(orderData) {
localStorage.setItem('orderData', JSON.stringify(orderData));

if (domCache.form) {
// Update ticket quantities
if (Array.isArray(orderData.tickets)) {
orderData.tickets.forEach(ticket => {
const select = domCache.form.querySelector(`[name="event_seat_${ticket.type.toLowerCase()}"]`);
if (select) select.value = ticket.quantity;
});
}

// Update add-ons
if (Array.isArray(orderData.addOns)) {
orderData.addOns.forEach(addon => {
const select = domCache.form.querySelector(`[name="addon_${addon.name}"]`);
if (select) select.value = addon.quantity;
});
}
}

// Update cart display without triggering saveOrderData
if (Array.isArray(orderData.tickets)) {
updateCartDisplay(
orderData.tickets,
orderData.total – (orderData.addOns?.reduce((sum, addon) => sum + (addon.price * addon.quantity), 0) || 0),
orderData.addOns?.reduce((sum, addon) => sum + (addon.price * addon.quantity), 0) || 0,
orderData.total,
false
);
}
}

// Add this function near the top-level functions
function saveOrderDataToStorage() {
try {
localStorage.setItem('fossettsOrderData', JSON.stringify(orderData));
} catch (e) {
console.error('Failed to save order data:', e);
}
}

function restoreSelectionsToUI() {
// Restore ticket quantities
if (orderData.tickets && Array.isArray(orderData.tickets)) {
orderData.tickets.forEach(ticket => {
// Use lowercase for ticket type to match attribute
const input = document.querySelector(`.quantity-value[data-section="${ticket.section}"][data-ticket-type="${ticket.type.toLowerCase()}"]`);
if (input) {
input.value = ticket.quantity;
}
});
}
// Restore add-on quantities
if (orderData.addOns && Array.isArray(orderData.addOns)) {
orderData.addOns.forEach(addon => {
const input = document.querySelector(`.quantity-value[data-addon-name="${addon.name}"]`);
if (input) {
input.value = addon.quantity;
}
});
}
}

// On DOMContentLoaded, load orderData from localStorage if present
const savedOrderData = localStorage.getItem('fossettsOrderData');
if (savedOrderData) {
try {
orderData = JSON.parse(savedOrderData);
restoreSelectionsToUI();
updateOrderSummary();
// Optionally, update UI for tickets/addons here if needed
} catch (e) {
console.error('Failed to parse saved order data:', e);
}
}

// Global function to show speech bubble warning
function showSpeechBubble(message) {
console.log('Showing speech bubble:', message);
const speechBubble = document.querySelector('.speech-bubble-warning');
if (!speechBubble) {
console.error('Speech bubble element not found');
return;
}

// Determine which step is active and position accordingly
const activeStep = document.querySelector('.step_1.active, .step_2.active, .step_3.active');
let nextButton = document.querySelector('.sticky-cart-bar .next-step');

// For Step 3, target the Buy Now button if it exists
if (activeStep && activeStep.classList.contains('step_3')) {
const buyNowButton = document.querySelector('.buy-now-btn.complete-purchase');
if (buyNowButton && buyNowButton.style.display !== 'none') {
nextButton = buyNowButton;
}
}

console.log('Active step:', activeStep?.className);
console.log('Next button found:', nextButton);
if (nextButton) {
console.log('Next button position:', nextButton.getBoundingClientRect());
}

// Use the same fixed positioning for both desktop and mobile
speechBubble.style.position = 'fixed';
speechBubble.style.bottom = '100px';
speechBubble.style.right = '10px';
speechBubble.style.top = 'auto';
speechBubble.style.left = 'auto';
speechBubble.style.transform = 'none';
speechBubble.style.zIndex = '9999';

// Move bubble to body
if (speechBubble.parentElement !== document.body) {
document.body.appendChild(speechBubble);
}

speechBubble.textContent = message;
speechBubble.style.display = 'block';

// No need to scroll since bubble is in fixed position

// Hide after 3 seconds
setTimeout(() => {
speechBubble.style.display = 'none';
}, 3000);
}

// Update the populateTicketSections function to include order info
function populateTicketSections(eventData) {
if (!domCache.ticketSections) return;

// Update order info block
const orderInfoBlock = document.querySelector('.order-info-block');
if (orderInfoBlock) {
orderInfoBlock.innerHTML = `
Order Information

${eventData.event_date_string}

${eventData.venue_title}

${eventData.venue_full}

`;
}

const html = eventData.tickets.map(section => {
const sectionId = section.title.toLowerCase().replace(/s+/g, '_');

return `

${section.title}
${section.seating_type}

Ticket Type
Price Per Person
Number of Tickets*

${section.ticket_types.map(type => `

${type.label}

Online Special
€${type.price}

+

`).join('')}

`}).join('');

domCache.ticketSections.innerHTML = html;

// Add event listeners for quantity buttons
document.querySelectorAll('.ticket-quantity-btn').forEach(button => {
button.addEventListener('click', function() {
const section = this.dataset.section;
const ticketType = this.dataset.ticketType;
const price = parseFloat(this.dataset.price);
const quantityInput = document.querySelector(`.quantity-value[data-section="${section}"][data-ticket-type="${ticketType}"]`);
const currentValue = parseInt(quantityInput.value);

if (this.classList.contains('minus') && currentValue > 0) {
quantityInput.value = currentValue – 1;
updateTicketQuantity(section, ticketType, price, currentValue – 1);
} else if (this.classList.contains('plus')) {
const maxValue = parseInt(quantityInput.getAttribute('max'));
if (currentValue < maxValue) {
quantityInput.value = currentValue + 1;
updateTicketQuantity(section, ticketType, price, currentValue + 1);
}
}
});
});
}

function populateAddOns(eventData) {
if (!domCache.addOnsGrid) return;
// Build all HTML at once
const html = `

${eventData.add_ons.map(addon => `

${addon.label}
${addon.description ? `${addon.description}` : ''}
€${addon.price.toFixed(2)}

+

`).join('')}
`;
domCache.addOnsGrid.innerHTML = html;
// Add event listeners for quantity buttons
document.querySelectorAll('.addon-list-item .quantity-btn').forEach(button => {
button.addEventListener('click', function() {
const addonName = this.dataset.addonName;
const price = parseFloat(this.dataset.price);
const quantityInput = document.querySelector(`.addon-list-item .quantity-value[data-addon-name="${addonName}"]`);
const currentValue = parseInt(quantityInput.value);
if (this.classList.contains('minus') && currentValue > 0) {
quantityInput.value = currentValue – 1;
updateAddonQuantity(addonName, price, currentValue – 1);
} else if (this.classList.contains('plus')) {
const maxValue = parseInt(quantityInput.getAttribute('max'));
if (currentValue < maxValue) {
quantityInput.value = currentValue + 1;
updateAddonQuantity(addonName, price, currentValue + 1);
}
}
});
});
}

function checkAndHandleAddOns(eventData) {
// If add_ons is empty or all are disabled, hide step_2
const hasActiveAddOns = eventData.add_ons && eventData.add_ons.some(addon => addon.enabled !== false);
const step2 = document.querySelector('.step_2');
if (!hasActiveAddOns) {
if (step2) {
step2.classList.add('inactive');
step2.style.display = 'none';
}
window.skipAddOnsStep = true;
} else {
if (step2) {
step2.classList.remove('inactive');
step2.style.display = '';
}
window.skipAddOnsStep = false;
}
}

function updateAddonQuantity(addonName, price, quantity) {
console.log('updateAddonQuantity called with:', { addonName, price, quantity });
console.log('globalAddOnsData:', globalAddOnsData);

// Find or create addon in orderData
const addonIndex = orderData.addOns.findIndex(a => a.name === addonName);

if (addonIndex === -1 && quantity > 0) {
// Find the addon data to get the proper label
const addonData = globalAddOnsData.find(addon => addon.name === addonName);
const label = addonData?.label || addonName;

console.log('Found addon data:', addonData);
console.log('Using label:', label);

// Add new addon
orderData.addOns.push({
name: addonName,
label: label,
price: price,
quantity: quantity
});
} else if (addonIndex !== -1) {
if (quantity > 0) {
orderData.addOns[addonIndex].quantity = quantity;
} else {
// Remove addon if quantity is zero
orderData.addOns.splice(addonIndex, 1);
}
}

// Update cart display
updateOrderSummary();
saveOrderDataToStorage();
}

// Event delegation for ticket and add-on selects
function handleSelectChange(event) {
if (event.target.matches('select[name^="event_seat_"], .add-on-quantity select')) {
updateOrderSummary();
}
}

// Initialize event listeners using event delegation
function initializeEventListeners() {
if (domCache.form) {
domCache.form.addEventListener('change', handleSelectChange);
// Add form submission handler
domCache.form.addEventListener('submit', handleFormSubmit);
}

// Add click handler for complete purchase button
const completePurchaseBtn = document.querySelector('.next-step.complete-purchase');
if (completePurchaseBtn) {
completePurchaseBtn.addEventListener('click', function(event) {
event.preventDefault();
handleFormSubmit(event);
});
}

// Add voucher validation
const applyVoucherBtn = document.querySelector('.apply-voucher-btn');
const voucherInput = document.querySelector('input[name="voucher"]');
const warningVoucher = document.querySelector('.warning-voucher');
const successVoucher = document.querySelector('.success-voucher');

if (applyVoucherBtn && voucherInput) {
applyVoucherBtn.addEventListener('click', async function() {
const voucherCode = voucherInput.value.trim();

if (!voucherCode) {
warningVoucher.style.display = 'none';
successVoucher.style.display = 'none';
return;
}

try {
// Get event IDs from URL
const urlParams = new URLSearchParams(window.location.search);
const eventId = urlParams.get('event_id');
const eventDateId = urlParams.get('event_date_id');

if (!eventId) {
throw new Error('Missing event information');
}

// Call backend to validate voucher
const response = await fetch(`/api/validate-voucher.php?event_id=${eventId}&voucher_code=${encodeURIComponent(voucherCode)}`);
const result = await response.json();

if (result.valid) {
warningVoucher.style.display = 'none';
successVoucher.style.display = 'block';
successVoucher.textContent = `Voucher Applied: ${voucherCode} – Discount on tickets ${result.discount_type === 'percentage' ? '-' + result.discount + '%' : '-€' + result.discount}`;
// Update total with discount
updateOrderSummary();
} else {
warningVoucher.style.display = 'block';
successVoucher.style.display = 'none';
}
} catch (error) {
console.error('Error validating voucher:', error);
warningVoucher.style.display = 'block';
successVoucher.style.display = 'none';
}
});

// Clear messages when input changes
voucherInput.addEventListener('input', function() {
warningVoucher.style.display = 'none';
successVoucher.style.display = 'none';
});
}
}

// Main initialization
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Starting initialization…');
initializeDomCache();
initializeEventListeners();

// Initialize speech bubble warning
const speechBubble = document.querySelector('.speech-bubble-warning');
if (speechBubble) {
speechBubble.style.display = 'none';
}

// Add form submission handler
const form = document.getElementById('event-order-form');
if (form) {
form.addEventListener('submit', handleFormSubmit);
}

// Add click handler for complete purchase button
const completePurchaseBtn = document.querySelector('.next-step.complete-purchase');
if (completePurchaseBtn) {
completePurchaseBtn.addEventListener('click', function(event) {
event.preventDefault();
handleFormSubmit(event);
});
}

// Initialize step navigation
const params = getUrlParams();
if (params.event_id && params.event_date_id) {
fetchEventData(params.event_id, params.event_date_id)
.then(eventData => {
checkAndHandleAddOns(eventData);
populateTicketSections(eventData);
populateAddOns(eventData);
restoreSelectionsToUI(); // {
console.error('Error loading event data:', error);

// Check if it's a booking closed error
if (error.message && error.message.includes('Online Booking Closed')) {
// Get the event date from the URL parameters to calculate countdown
const params = getUrlParams();
const url = `/api/events.php?event_id=${params.event_id}&event_date_id=${params.event_date_id}`;

fetch(url)
.then(response => response.json())
.then(data => {
const eventDate = data.event_dates.find(date => date.event_date_id === parseInt(params.event_date_id));
if (eventDate) {
const showtime = new Date(eventDate.event_datetime);
const now = new Date();
const timeDiff = showtime – now;
const minutesUntilShow = Math.floor(timeDiff / (1000 * 60));

// Display online closed message with countdown
const ticketSections = document.getElementById('ticket-sections');
if (ticketSections) {
ticketSections.innerHTML = `

ONLINE CLOSED – Starts in ${minutesUntilShow} min

Go to Boxoffice

Box Office Information:
• Open 1 hour before showtime
• Cash and card payments accepted
• Subject to availability

`;
}
}
})
.catch(() => {
// Fallback if we can't get the countdown
const ticketSections = document.getElementById('ticket-sections');
if (ticketSections) {
ticketSections.innerHTML = `

ONLINE CLOSED

This show is starting within 30 minutes.

Go to Boxoffice

`;
}
});
} else {
// Show generic error message
showSpeechBubble('Error loading event data. Please refresh the page.');
}
});
}

// Wheelchair user logic (number input with +/-)
const wheelchairCountInput = document.getElementById('wheelchair-user-count');
const wheelchairMinus = document.getElementById('wheelchair-minus');
const wheelchairPlus = document.getElementById('wheelchair-plus');
if (wheelchairCountInput && wheelchairMinus && wheelchairPlus) {
wheelchairMinus.addEventListener('click', function() {
let val = parseInt(wheelchairCountInput.value) || 0;
if (val > 0) wheelchairCountInput.value = val – 1;
});
wheelchairPlus.addEventListener('click', function() {
let val = parseInt(wheelchairCountInput.value) || 0;
wheelchairCountInput.value = val + 1;
});
}
});

function getWheelchairUserData() {
const wheelchairCountInput = document.getElementById('wheelchair-user-count');
let wheelchairUsers = 0;
if (wheelchairCountInput) {
wheelchairUsers = parseInt(wheelchairCountInput.value) || 0;
}
return wheelchairUsers;
}

// In handleFormSubmit or wherever orderData is finalized before sending to backend:
// orderData.wheelchair_users = getWheelchairUserData();

// Cart functionality
let cartInitialized = false;
let isToggling = false;

function toggleCart(event) {
if (isToggling) return;
isToggling = true;

if (event) {
event.preventDefault();
event.stopPropagation();
}

console.log('Toggling cart', {
cartInitialized,
event: event ? event.type : 'manual',
target: event ? event.target : 'none'
});

const cartSlideOut = document.querySelector('.cart-slide-out');
const cartToggle = document.querySelector('.cart-toggle');
const cartToggleIcon = document.querySelector('.cart-toggle-icon');

if (!cartSlideOut || !cartToggle) {
console.error('Cart elements not found:', { cartSlideOut, cartToggle });
isToggling = false;
return;
}

const isActive = cartSlideOut.classList.contains('active');

if (isActive) {
cartSlideOut.classList.remove('active');
cartToggle.classList.remove('active');
if (cartToggleIcon) cartToggleIcon.classList.remove('active');
} else {
cartSlideOut.classList.add('active');
cartToggle.classList.add('active');
if (cartToggleIcon) cartToggleIcon.classList.add('active');
}

setTimeout(() => {
isToggling = false;
}, 100);
}

function initializeCart() {
if (cartInitialized) {
console.log('Cart already initialized');
return;
}

console.log('Initializing cart functionality');

// Remove any existing event listeners
const cartToggle = document.querySelector('.cart-toggle');
const innerCartToggle = document.querySelector('.cart-toggle.inner');

if (cartToggle) {
cartToggle.removeEventListener('click', toggleCart);
cartToggle.addEventListener('click', toggleCart);
}

if (innerCartToggle) {
innerCartToggle.removeEventListener('click', toggleCart);
innerCartToggle.addEventListener('click', toggleCart);
}

// Add click outside handler
document.removeEventListener('click', handleOutsideClick);
document.addEventListener('click', handleOutsideClick);

cartInitialized = true;
console.log('Cart initialization complete');
}

function handleOutsideClick(event) {
const cartSlideOut = document.querySelector('.cart-slide-out');
const cartToggle = document.querySelector('.cart-toggle');

if (cartSlideOut && cartSlideOut.classList.contains('active')) {
if (!cartSlideOut.contains(event.target) && !cartToggle.contains(event.target)) {
toggleCart();
}
}
}

// Initialize cart when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Initializing cart…');
initializeCart();
});

// Step Navigation
function goToStep(stepNumber) {
console.log('Navigating to step:', stepNumber);

// Validate step number
if (stepNumber < 1 || stepNumber > 3) {
console.warn('Invalid step number:', stepNumber);
return;
}

// If skipping step 2
if (window.skipAddOnsStep) {
if (stepNumber === 2) stepNumber = 3;
if (stepNumber === 0) stepNumber = 1;
}

// Validate ticket selection when moving to step 2 or 3
if (stepNumber === 2 || stepNumber === 3) {
let totalTickets = 0;
const quantityInputs = document.querySelectorAll('.quantity-value');

quantityInputs.forEach(input => {
totalTickets += parseInt(input.value) || 0;
});

if (totalTickets === 0) {
showSpeechBubble('Please select at least one ticket before proceeding');
return;
}
}

// Hide all steps first
const steps = document.querySelectorAll('.step_1, .step_2, .step_3');
steps.forEach(step => {
step.classList.remove('active');
console.log('Hiding step:', step.className);
});

// Show selected step
const selectedStep = document.querySelector(`.step_${stepNumber}`);
if (selectedStep) {
// Force a reflow to ensure the transition works
selectedStep.offsetHeight;
selectedStep.classList.add('active');
console.log('Showing step:', selectedStep.className);

// Initialize Stripe when reaching Step 3
if (stepNumber === 3) {
console.log('Reached Step 3, initializing Stripe…');
initializeStripe();
}
} else {
console.error(`Step ${stepNumber} element not found`);
return;
}

// Update step indicator
const stepIndicators = document.querySelectorAll('.step-indicator');
stepIndicators.forEach(indicator => {
indicator.textContent = `Step ${stepNumber} of 3`;
});

// Update navigation buttons
updateNavigationButtons(stepNumber);

// Scroll to top of the form
const form = document.getElementById('event-order-form');
if (form) {
form.scrollIntoView({ behavior: 'smooth' });
}
}

function updateNavigationButtons(currentStep) {
console.log('Updating navigation buttons for step:', currentStep);

// Update sticky navigation
const stickyPrev = document.querySelector('.sticky-cart-bar .prev-step');
const stickyNext = document.querySelector('.sticky-cart-bar .next-step');
const buyNowBtn = document.querySelector('.buy-now-btn');

if (stickyPrev) {
stickyPrev.style.display = 'block';
if (currentStep === 1) {
stickyPrev.innerHTML = '';
stickyPrev.onclick = function(event) {
event.preventDefault();
window.location.href = '/index.html';
};
} else {
stickyPrev.innerHTML = '';
stickyPrev.onclick = function(event) {
event.preventDefault();
goToStep(currentStep – 1);
};
}
}

if (stickyNext) {
if (currentStep === 3) {
// Hide the next button in sticky nav on step 3 but maintain space
stickyNext.style.visibility = 'hidden';
stickyNext.classList.add('hide-on-step3');
} else {
stickyNext.style.visibility = 'visible';
stickyNext.classList.remove('hide-on-step3');
stickyNext.innerHTML = '';
stickyNext.classList.remove('complete-purchase');
stickyNext.onclick = function(event) {
event.preventDefault();
goToStep(currentStep + 1);
};
}
}

// Show/hide buy now button
if (buyNowBtn) {
if (currentStep === 3) {
buyNowBtn.style.display = 'block';
buyNowBtn.onclick = function(event) {
event.preventDefault();
handleFormSubmit(event);
};
} else {
buyNowBtn.style.display = 'none';
}
}
}

// Initialize step navigation when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Initializing step navigation…');

// Ensure Step 1 is active by default
const step1 = document.querySelector('.step_1');
const step2 = document.querySelector('.step_2');
const step3 = document.querySelector('.step_3');

if (step1) {
step1.classList.add('active');
console.log('Initialized Step 1 as active');
}

if (step2) {
step2.classList.remove('active');
console.log('Initialized Step 2 as hidden');
}

if (step3) {
step3.classList.remove('active');
console.log('Initialized Step 3 as hidden');
}

// Add click handlers to step navigation buttons
const nextButtons = document.querySelectorAll('.next-step');
const prevButtons = document.querySelectorAll('.prev-step');

nextButtons.forEach(button => {
button.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation(); // Prevent event bubbling

// Find the current active step
const activeStep = document.querySelector('.step_1.active, .step_2.active, .step_3.active');
if (!activeStep) {
console.error('No active step found');
return;
}

const currentStepNumber = parseInt(activeStep.className.match(/step_(d+)/)?.[1] || '1');
console.log('Current step:', currentStepNumber);

// Validate ticket selection before proceeding
if (currentStepNumber === 1) {
let totalTickets = 0;
const quantityInputs = document.querySelectorAll('.quantity-value');

quantityInputs.forEach(input => {
totalTickets += parseInt(input.value) || 0;
});

if (totalTickets === 0) {
showSpeechBubble('Please select at least one ticket before proceeding');
return;
}
}

if (currentStepNumber < 3) {
// Add a small delay to prevent multiple transitions
setTimeout(() => {
goToStep(currentStepNumber + 1);
}, 100);
}
});
});

prevButtons.forEach(button => {
button.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation(); // Prevent event bubbling

// Find the current active step
const activeStep = document.querySelector('.step_1.active, .step_2.active, .step_3.active');
if (!activeStep) {
console.error('No active step found');
return;
}

const currentStepNumber = parseInt(activeStep.className.match(/step_(d+)/)?.[1] || '1');
console.log('Current step:', currentStepNumber);

if (currentStepNumber > 1) {
// If skipping step 2, go directly to step 1 from step 3
let targetStep = currentStepNumber – 1;
if (window.skipAddOnsStep && currentStepNumber === 3) {
targetStep = 1;
}
setTimeout(() => {
goToStep(targetStep);
}, 100);
}
});
});

// Initialize navigation buttons for the first step
updateNavigationButtons(1);
});

// Stripe Integration
let stripe;
let elements;
let stripeInitialized = false;

async function initializeStripe() {
if (stripeInitialized) {
console.log('Stripe already initialized');
return;
}

console.log('Initializing Stripe with Express Checkout…');
try {
// Initialize Stripe with your publishable key
stripe = Stripe('pk_live_KSh8MRi76Kliuk4fUpBZ23GS', {
apiVersion: '2023-10-16',
stripeAccount: 'acct_1C7mK9IQtQgABICR'
});

// Create elements with appearance configuration
elements = stripe.elements({
appearance: {
theme: 'stripe',
variables: {
colorPrimary: '#ed1e26',
colorBackground: '#ffffff',
colorText: '#30313d',
colorDanger: '#df1b41',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
spacingUnit: '4px',
borderRadius: '10px'
}
}
});

// Add custom CSS for larger payment buttons
const style = document.createElement('style');
style.textContent = `
.GooglePayButton {
border: 0 !important;
cursor: pointer !important;
max-height: 84px !important;
min-height: 52px !important;
position: relative !important;
}

.ApplePayButton {
border: 0 !important;
cursor: pointer !important;
max-height: 84px !important;
min-height: 52px !important;
position: relative !important;
}

#payment-request-button {
max-height: 84px !important;
min-height: 52px !important;
}

#payment-request-button button {
max-height: 84px !important;
min-height: 52px !important;
transform: scale(1.2) !important;
transform-origin: center !important;
}
`;
document.head.appendChild(style);

// Create and mount card elements with improved styling
const cardNumber = elements.create('cardNumber', {
placeholder: 'Card number',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card number element is properly mounted
const cardNumberContainer = document.getElementById('card-number');
if (cardNumberContainer) {
// Clear any existing content
cardNumberContainer.innerHTML = '';
cardNumber.mount('#card-number');
console.log('Card number element mounted successfully');
} else {
console.error('Card number container not found');
}

const cardExpiry = elements.create('cardExpiry', {
placeholder: 'MM/YY',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card expiry element is properly mounted
const cardExpiryContainer = document.getElementById('card-expiry');
if (cardExpiryContainer) {
// Clear any existing content
cardExpiryContainer.innerHTML = '';
cardExpiry.mount('#card-expiry');
console.log('Card expiry element mounted successfully');
} else {
console.error('Card expiry container not found');
}

const cardCvc = elements.create('cardCvc', {
placeholder: 'CVC',
style: {
base: {
fontSize: '20px',
color: '#30313d',
lineHeight: '50px',
padding: '0 15px',
'::placeholder': {
color: '#aab7c4'
}
}
}
});

// Ensure card CVC element is properly mounted
const cardCvcContainer = document.getElementById('card-cvc');
if (cardCvcContainer) {
// Clear any existing content
cardCvcContainer.innerHTML = '';
cardCvc.mount('#card-cvc');
console.log('Card CVC element mounted successfully');
} else {
console.error('Card CVC container not found');
}

// Add event listeners to card elements for debugging
cardNumber.on('ready', () => {
console.log('Card number element ready');
// Fix accessibility warnings for hidden Stripe inputs
fixStripeAccessibilityWarnings();
});

cardNumber.on('focus', () => {
console.log('Card number focused');
});

cardNumber.on('blur', () => {
console.log('Card number blurred');
});

// Function to fix Stripe accessibility warnings
function fixStripeAccessibilityWarnings() {
// Remove aria-hidden from Stripe's hidden inputs that are focused
const hiddenInputs = document.querySelectorAll('.__PrivateStripeElement-input[aria-hidden="true"]');
hiddenInputs.forEach(input => {
// Remove aria-hidden attribute to prevent accessibility warnings
input.removeAttribute('aria-hidden');
// Ensure they're properly hidden with CSS instead
input.style.position = 'absolute';
input.style.left = '-9999px';
input.style.top = '-9999px';
input.style.width = '1px';
input.style.height = '1px';
input.style.opacity = '0';
input.style.pointerEvents = 'none';
// Prevent focus
input.setAttribute('tabindex', '-1');
input.style.pointerEvents = 'none';
});
}

// Set up MutationObserver to catch dynamically created Stripe elements
function setupStripeAccessibilityObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if the added node is a Stripe hidden input
if (node.classList && node.classList.contains('__PrivateStripeElement-input')) {
if (node.getAttribute('aria-hidden') === 'true') {
node.removeAttribute('aria-hidden');
node.style.position = 'absolute';
node.style.left = '-9999px';
node.style.top = '-9999px';
node.style.width = '1px';
node.style.height = '1px';
node.style.opacity = '0';
node.style.pointerEvents = 'none';
}
}
// Also check child elements
const hiddenInputs = node.querySelectorAll && node.querySelectorAll('.__PrivateStripeElement-input[aria-hidden="true"]');
if (hiddenInputs) {
hiddenInputs.forEach(input => {
input.removeAttribute('aria-hidden');
input.style.position = 'absolute';
input.style.left = '-9999px';
input.style.top = '-9999px';
input.style.width = '1px';
input.style.height = '1px';
input.style.opacity = '0';
input.style.pointerEvents = 'none';
});
}
}
});
}
});
});

// Start observing the document body for changes
observer.observe(document.body, {
childList: true,
subtree: true
});

return observer;
}

// — Stripe Express Checkout Element (New Implementation) —
// Calculate the total for Express Checkout
function getTotalAmountForExpressCheckout() {
let subtotal = 0;
orderData.tickets.forEach(ticket => {
if (ticket.quantity > 0) {
subtotal += ticket.quantity * ticket.price;
}
});
let addOnsTotal = 0;
orderData.addOns.forEach(addon => {
addOnsTotal += addon.quantity * addon.price;
});
let totalAmount = subtotal + addOnsTotal;

// Apply voucher discount if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (discountMatch) {
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];
if (discountType === '%') {
totalAmount = subtotal * (1 – discountValue / 100) + addOnsTotal;
} else {
totalAmount = Math.max(0, subtotal – discountValue) + addOnsTotal;
}
}
}
return Math.round(totalAmount * 100); // in cents
}

// Create Payment Request Button (legacy API – works without clientSecret)
const paymentRequestAmount = getTotalAmountForExpressCheckout();
console.log('Creating Payment Request Button with amount:', paymentRequestAmount);

// Create payment request
const paymentRequest = stripe.paymentRequest({
country: 'IE',
currency: 'eur',
total: {
label: 'Fossetts Circus Tickets',
amount: paymentRequestAmount,
},
requestPayerName: true,
requestPayerEmail: true,
requestPayerPhone: true,
});

// Create payment request button
let paymentRequestButton;
try {
console.log('Creating payment request button…');
paymentRequestButton = elements.create('paymentRequestButton', {
paymentRequest: paymentRequest,
style: {
paymentRequestButton: {
type: 'default',
theme: 'dark',
height: '44px'
}
}
});
console.log('Payment Request Button created successfully:', paymentRequestButton);
} catch (error) {
console.error('Error creating Payment Request Button:', error);
console.error('Error details:', {
message: error.message,
stack: error.stack,
name: error.name
});
return;
}

// Check if payment request is supported
paymentRequest.canMakePayment().then(function(result) {
console.log('Payment Request canMakePayment result:', result);
// TEMPORARY: Always hide Google Pay and Apple Pay button for debugging
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
return; // Prevent any further logic from showing the button
// — original logic below (commented out) —
/*
if (result) {
console.log('Payment Request is supported, mounting button…');
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer && paymentRequestAmount > 0) {
paymentRequestContainer.innerHTML = '';
paymentRequestContainer.style.display = 'block';
try {
paymentRequestButton.mount('#payment-request-button');
console.log('Payment Request Button mounted successfully');
} catch (error) {
console.error('Error mounting Payment Request Button:', error);
}
} else {
console.log('Payment Request container not found or amount is 0');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
}
} else {
console.log('Payment Request is not supported');
const paymentRequestContainer = document.getElementById('payment-request-button');
if (paymentRequestContainer) {
paymentRequestContainer.style.display = 'none';
}
}
*/
});

// Handle payment request events
paymentRequest.on('paymentmethod', async function(ev) {
console.log('Payment Request paymentmethod event:', ev);

try {
// Prepare form data
const form = document.getElementById('event-order-form');
const formData = new FormData(form);

// Add wheelchair user count
const wheelchairUsers = getWheelchairUserData();
if (wheelchairUsers > 0) {
formData.append('wheelchair_users', wheelchairUsers);
}

// Add payment token (backend expects payment_token)
formData.append('payment_token', ev.paymentMethod.id);

// Add event and date ids from URL parameters
const urlParams = getUrlParams();
const eventId = urlParams.event_id;
const eventDateId = urlParams.event_date_id;

// Add event and date ids to form data
formData.append('event_id', eventId);
formData.append('event_date_id', eventDateId);

// Add customer information from Payment Request
formData.append('customer_name', ev.payerName || '');
formData.append('customer_email', ev.payerEmail || '');
formData.append('customer_confirm_email', ev.payerEmail || '');
formData.append('customer_phone', ev.payerPhone || '');

// Collect ticket quantities (same as regular form submission)
console.log('Collecting ticket quantities for Payment Request…');
const sectionQuantities = {};
const quantityInputs = document.querySelectorAll('.quantity-value[data-section][data-ticket-type]');

quantityInputs.forEach(input => {
const section = input.dataset.section;
const ticketType = input.dataset.ticketType;
const quantity = parseInt(input.value) || 0;

if (!sectionQuantities[section]) {
sectionQuantities[section] = {};
}

if (ticketType && quantity > 0) {
sectionQuantities[section][ticketType.toLowerCase()] = quantity;
}

console.log('Processing ticket input:', {
section,
ticketType,
quantity,
value: input.value
});
});

console.log('Section quantities:', sectionQuantities);

// Send all ticket types dynamically with seating area information
Object.entries(sectionQuantities).forEach(([section, sectionData]) => {
Object.entries(sectionData).forEach(([ticketType, quantity]) => {
// Convert spaces to underscores in the ticket type name for the form field
// The server expects: event_seat_{seating_area}_{ticket_type_with_underscores}
const cleanTicketType = ticketType.replace(/s+/g, '_');
formData.append(`event_seat_${section}_${cleanTicketType}`, quantity);

console.log('Added form field:', `event_seat_${section}_${cleanTicketType}`, '=', quantity);
console.log('Ticket type details:', {
section: section,
originalTicketType: ticketType,
cleanTicketType: cleanTicketType,
quantity: quantity,
fieldName: `event_seat_${section}_${cleanTicketType}`
});
});
});

// Add add-ons
console.log('Collecting add-ons for Payment Request…');
const addonInputs = document.querySelectorAll('.addon-list-item .quantity-value');
console.log('Found addon inputs:', addonInputs.length);

addonInputs.forEach(input => {
const addonId = input.dataset.addonId;
const addonName = input.dataset.addonName;
const quantity = parseInt(input.value) || 0;

console.log('Processing addon input:', {
addonId,
addonName,
quantity,
value: input.value
});

if (addonId && quantity > 0) {
formData.append(`addon_${addonId}`, quantity);
console.log(`Added addon_${addonId} = ${quantity}`);
}
});

// Add voucher if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const voucherMatch = voucherText.match(/Voucher: ([A-Z0-9]+)/);
if (voucherMatch) {
formData.append('voucher_code', voucherMatch[1]);
}
}

// Submit order to server
const response = await fetch(`/order.php?event_id=${eventId}&event_date_id=${eventDateId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});

const result = await response.json();
if (result.success) {
// Complete the payment request successfully
ev.complete('success');
window.location.href = result.redirect_url;
} else {
// Complete the payment request with error
ev.complete('fail');
showSpeechBubble(result.message || 'Payment failed. Please try again.');
}
} catch (error) {
console.error('Error handling Payment Request payment:', error);
// Complete the payment request with error
ev.complete('fail');
showSpeechBubble('Payment processing failed. Please try again.');
}
});

paymentRequest.on('cancel', function() {
console.log('Payment Request cancelled');
});

// Update Payment Request amount when order changes
function updatePaymentRequestAmount() {
const newAmount = getTotalAmountForExpressCheckout();
paymentRequest.update({
total: {
label: 'Fossetts Circus Tickets',
amount: newAmount,
}
});
}

// Store the update function globally for use in order updates
window.updatePaymentRequestAmount = updatePaymentRequestAmount;

stripeInitialized = true;
console.log('Stripe Payment Request Button initialized successfully');

// Set up observer to catch dynamically created Stripe elements
const stripeObserver = setupStripeAccessibilityObserver();

// Suppress specific Stripe accessibility warnings
const originalWarn = console.warn;
console.warn = function(…args) {
const message = args.join(' ');
if (message.includes('aria-hidden') && message.includes('Stripe')) {
// Suppress Stripe aria-hidden warnings
return;
}
originalWarn.apply(console, args);
};

// Fix accessibility warnings after all elements are mounted
setTimeout(() => {
fixStripeAccessibilityWarnings();
}, 1000);

} catch (error) {
console.error('Error initializing Stripe Express Checkout:', error);
throw error;
}
}

// Initialize payment functionality when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded – Initializing payment…');

// Initialize Stripe
initializeStripe();

// Add form submit handler
const form = document.getElementById('event-order-form');
if (form) {
form.addEventListener('submit', handleFormSubmit);
}

// Add click handler for complete purchase button
const completePurchaseBtn = document.querySelector('.next-step.complete-purchase');
if (completePurchaseBtn) {
completePurchaseBtn.addEventListener('click', function(event) {
event.preventDefault();
handleFormSubmit(event);
});
}
});

async function handleFormSubmit(event) {
event.preventDefault();
console.log('Form submission started');

// Ensure order summary is up to date before checking total
updateOrderSummary();

// Recalculate total directly before checking minimum
let subtotal = 0;
orderData.tickets.forEach(ticket => {
if (ticket.quantity > 0) {
subtotal += ticket.quantity * ticket.price;
}
});
let addOnsTotal = 0;
orderData.addOns.forEach(addon => {
addOnsTotal += addon.quantity * addon.price;
});
let totalAmount = subtotal + addOnsTotal;

// Apply voucher discount if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (discountMatch) {
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];
if (discountType === '%') {
totalAmount = subtotal * (1 – discountValue / 100) + addOnsTotal;
} else {
totalAmount = Math.max(0, subtotal – discountValue) + addOnsTotal;
}
}
}

// Log detailed payment calculation
console.log('Payment calculation details:', {
ticketSubtotal: subtotal,
addOnsTotal: addOnsTotal,
totalBeforeVoucher: subtotal + addOnsTotal,
totalAfterVoucher: totalAmount,
ticketDetails: orderData.tickets.map(t => ({
type: t.type,
quantity: t.quantity,
price: t.price,
subtotal: t.quantity * t.price
})),
addonDetails: orderData.addOns.map(a => ({
name: a.name,
quantity: a.quantity,
price: a.price,
subtotal: a.quantity * a.price
}))
});

// Validate minimum payment amount
if (totalAmount < 0.5) {
const errorMessage = `The minimum payment amount is €0.50. Current total: €${totalAmount.toFixed(2)}`;
console.error(errorMessage);
showSpeechBubble(errorMessage);
return;
}

// Validate form
const form = document.getElementById('event-order-form');
if (!form) {
console.error('Form not found');
return;
}

// Get form elements safely
const customerName = form.querySelector('[name="customer_name"]')?.value?.trim();
const customerPhone = form.querySelector('[name="customer_phone"]')?.value?.trim();
const email = document.getElementById('email_address')?.value?.trim();
const confirmEmail = document.getElementById('confirm_email_address')?.value?.trim();

// Validate required fields
if (!customerName) {
showSpeechBubble('Please enter your full name');
return;
}
if (!customerPhone) {
showSpeechBubble('Please enter your phone number');
return;
}
if (!email) {
showSpeechBubble('Please enter your email address');
return;
}
if (!confirmEmail) {
showSpeechBubble('Please confirm your email address');
return;
}

// Validate email match
if (email !== confirmEmail) {
showSpeechBubble('Email addresses do not match');
return;
}

// Validate email format
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
if (!emailRegex.test(email)) {
showSpeechBubble('Please enter a valid email address');
return;
}

// Validate phone number (basic validation)
const phoneRegex = /^[ds+()-]{8,}$/;
if (!phoneRegex.test(customerPhone)) {
showSpeechBubble('Please enter a valid phone number');
return;
}

// Validate card details
if (!stripe || !elements) {
showSpeechBubble('Payment system not initialized. Please refresh the page.');
return;
}

const cardNumber = elements.getElement('cardNumber');
const cardExpiry = elements.getElement('cardExpiry');
const cardCvc = elements.getElement('cardCvc');

if (!cardNumber || !cardExpiry || !cardCvc) {
showSpeechBubble('Please enter all card details');
return;
}

// Get submit button (check both locations)
const submitButton = document.querySelector('.buy-now-btn') || document.querySelector('.next-step.complete-purchase');
if (!submitButton) {
console.error('Submit button not found');
showSpeechBubble('Error: Submit button not found. Please refresh the page.');
return;
}

// Disable submit button
submitButton.disabled = true;
submitButton.textContent = 'Processing…';

try {
// Get event IDs from URL
const urlParams = new URLSearchParams(window.location.search);
const eventId = urlParams.get('event_id');
const eventDateId = urlParams.get('event_date_id');

if (!eventId || !eventDateId) {
throw new Error('Missing event information');
}

// Create FormData object
const formData = new FormData();

// Add form data
formData.append('customer_name', customerName);
formData.append('customer_email', email);
formData.append('customer_confirm_email', confirmEmail);
formData.append('customer_phone', customerPhone);
formData.append('event_id', eventId);
formData.append('event_date_id', eventDateId);

// Sum ticket quantities across all sections – DYNAMIC APPROACH
// Instead of hardcoded mappings, we'll send all ticket types as they are

// Debug log for ticket collection
console.log('Collecting ticket quantities…');

// Collect all quantities by section and type
const sectionQuantities = {};
const quantityInputs = document.querySelectorAll('.quantity-value[data-section][data-ticket-type]');

quantityInputs.forEach(input => {
const section = input.dataset.section;
const ticketType = input.dataset.ticketType;
const quantity = parseInt(input.value) || 0;

if (!sectionQuantities[section]) {
sectionQuantities[section] = {};
}

if (ticketType && quantity > 0) {
// Store the original ticket type name as it appears in the database
sectionQuantities[section][ticketType] = quantity;
}

console.log('Processing ticket input:', {
section,
ticketType,
quantity,
value: input.value
});
});

// Log the collected ticket counts
console.log('Section quantities:', sectionQuantities);

// Send all ticket types dynamically with seating area information
Object.entries(sectionQuantities).forEach(([section, sectionData]) => {
Object.entries(sectionData).forEach(([ticketType, quantity]) => {
// Convert spaces to underscores in the ticket type name for the form field
// The server expects: event_seat_{seating_area}_{ticket_type_with_underscores}
const cleanTicketType = ticketType.replace(/s+/g, '_');
formData.append(`event_seat_${section}_${cleanTicketType}`, quantity);

console.log('Added form field:', `event_seat_${section}_${cleanTicketType}`, '=', quantity);
console.log('Ticket type details:', {
section: section,
originalTicketType: ticketType,
cleanTicketType: cleanTicketType,
quantity: quantity,
fieldName: `event_seat_${section}_${cleanTicketType}`
});
});
});

// Add add-ons
console.log('Collecting add-ons…');
const addonInputs = document.querySelectorAll('.addon-list-item .quantity-value');
console.log('Found addon inputs:', addonInputs.length);

addonInputs.forEach(input => {
const addonId = input.dataset.addonId;
const addonName = input.dataset.addonName;
const quantity = parseInt(input.value) || 0;

console.log('Processing addon input:', {
addonId,
addonName,
quantity,
value: input.value,
dataset: input.dataset
});

if (addonId && quantity > 0) {
formData.append(`addon_${addonId}`, quantity);
console.log(`Added addon_${addonId} = ${quantity}`);
}
});

// Add voucher if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherInput = document.querySelector('input[name="voucher"]');
if (voucherInput) {
formData.append('voucher', voucherInput.value.trim());
}
}

// Add wheelchair user count
const wheelchairUsers = getWheelchairUserData();
if (wheelchairUsers > 0) {
formData.append('wheelchair_users', wheelchairUsers);
}

console.log('Submitting form data:', {
eventId,
eventDateId,
customerName,
email,
customerPhone,
formData: Object.fromEntries(formData)
});

// Debug: Log all form data entries
console.log('All form data entries:');
for (let [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}

// Create payment method using existing card elements
const { error: paymentMethodError, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardNumber,
billing_details: {
name: customerName,
email: email,
phone: customerPhone
}
});

if (paymentMethodError) {
throw new Error(paymentMethodError.message);
}

// Add payment token to form data
formData.append('payment_token', paymentMethod.id);

// Submit order to server
const response = await fetch(`/order.php?event_id=${eventId}&event_date_id=${eventDateId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});

console.log('Server response status:', response.status);
const responseText = await response.text();
console.log('Server response text:', responseText);

let result;
try {
result = JSON.parse(responseText);
console.log('Parsed server response:', result);

// Log detailed error information if present
if (!result.success) {
console.error('Server error details:', {
message: result.message,
status: result.status,
total: totalAmount,
formData: Object.fromEntries(formData)
});
}
} catch (e) {
console.error('Failed to parse server response:', e);
console.error('Raw response:', responseText);
throw new Error('Server returned an invalid response');
}

if (!result) {
throw new Error('Empty response from server');
}

if (result.error) {
throw new Error(result.error);
}

if (result.requires_action) {
// Handle 3D Secure authentication
const { error: confirmError } = await stripe.confirmCardPayment(
result.payment_intent_client_secret
);

if (confirmError) {
throw new Error(confirmError.message);
}

// Retry the order submission with the payment intent ID
formData.append('payment_intent_id', result.payment_intent_id);
const finalResponse = await fetch(`/order.php?event_id=${eventId}&event_date_id=${eventDateId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});

if (!finalResponse.ok) {
throw new Error(`Server error: ${finalResponse.status}`);
}

const finalResult = await finalResponse.json();
if (!finalResult.success) {
throw new Error(finalResult.message || 'Order processing failed');
}

// Redirect directly to the detailed success page
if (finalResult.redirect_url) {
window.location.href = finalResult.redirect_url;
} else {
// Fallback if no redirect URL
const successMessage = document.createElement('div');
successMessage.className = 'success-message';
successMessage.innerHTML = `
Order Successful!
Thank you for your purchase. Your order has been processed successfully.
Order Reference: ${finalResult.order_code || 'N/A'}
A confirmation email has been sent to ${email}

Return to Home

`;

// Replace form content with success message
const form = document.getElementById('event-order-form');
if (form) {
form.innerHTML = '';
form.appendChild(successMessage);
}
}
} else if (result.success) {
// Redirect directly to the detailed success page
if (result.redirect_url) {
window.location.href = result.redirect_url;
} else {
// Fallback if no redirect URL
const successMessage = document.createElement('div');
successMessage.className = 'success-message';
successMessage.innerHTML = `
Order Successful!
Thank you for your purchase. Your order has been processed successfully.
Order Reference: ${result.order_code || 'N/A'}
A confirmation email has been sent to ${email}

Return to Home

`;

// Replace form content with success message
const form = document.getElementById('event-order-form');
if (form) {
form.innerHTML = '';
form.appendChild(successMessage);
}
}
} else {
throw new Error(result.message || 'Order processing failed');
}

} catch (error) {
console.error('Payment error:', error);
showSpeechBubble(error.message || 'An error occurred during payment processing');
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = 'Complete Purchase';
}
}
}

function updateCartDisplay(ticketData, subtotal, addOnsTotal, total, shouldSave = true) {
console.log('updateCartDisplay called with:', { ticketData, subtotal, addOnsTotal, total, shouldSave });
console.log('orderData.addOns:', orderData.addOns);

// Generate cart items HTML
let cartItemsHTML = '';

// Add ticket items
if (Array.isArray(ticketData)) {
ticketData.forEach(ticket => {
// Format the section name (remove underscores and capitalize)
const sectionName = ticket.section
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');

// Dynamic ticket type mapping – handle any ticket type
let typeLabel = ticket.type;

// Only apply mapping for legacy ticket types, keep new types as-is
const legacyTypeMapping = {
'FULL': 'Adult',
'CHILD': 'Child',
'SENIOR': 'Senior'
};

if (legacyTypeMapping[ticket.type]) {
typeLabel = legacyTypeMapping[ticket.type];
}

cartItemsHTML += `

${typeLabel} – ${sectionName}
€${ticket.price.toFixed(2)} each remove

x${ticket.quantity}
€${(ticket.price * ticket.quantity).toFixed(2)}

`;
});
}

// Add add-ons
if (Array.isArray(orderData.addOns)) {
console.log('Processing add-ons in updateCartDisplay:', orderData.addOns);
orderData.addOns.forEach(addon => {
if (addon.quantity > 0) {
const displayName = addon.label || addon.name;
console.log('Addon display name:', displayName, 'for addon:', addon);
cartItemsHTML += `

${displayName}
€${addon.price.toFixed(2)} each remove

x${addon.quantity}
€${(addon.price * addon.quantity).toFixed(2)}

`;
}
});
}

// Add voucher discount if present
const successVoucher = document.querySelector('.success-voucher');
if (successVoucher && successVoucher.style.display === 'block') {
const voucherText = successVoucher.textContent;
const voucherMatch = voucherText.match(/Voucher Applied: (w+)/);
const discountMatch = voucherText.match(/-(d+)(%|€)/);
if (voucherMatch && discountMatch) {
const voucherCode = voucherMatch[1];
const discountValue = parseFloat(discountMatch[1]);
const discountType = discountMatch[2];
let discountAmount = 0;

if (discountType === '%') {
// Apply discount only to ticket subtotal
discountAmount = subtotal * (discountValue / 100);
} else {
// For fixed amount, apply to ticket total only
discountAmount = Math.min(discountValue, subtotal);
}

cartItemsHTML += `

Voucher Applied: ${voucherCode}
Discount on tickets -${discountValue}${discountType} (remove)

-€${discountAmount.toFixed(2)}

`;
}
}

// Update all cart items containers
const cartItemsContainers = document.querySelectorAll('.cart-items');
cartItemsContainers.forEach(container => {
if (container) {
container.innerHTML = cartItemsHTML;
}
});

// Update the main order summary total
const summaryTotal = document.getElementById('summary-total-amount');
if (summaryTotal) {
summaryTotal.textContent = `€${total.toFixed(2)}`;
}

// Update the sticky cart bar total
const stickyCartTotal = document.querySelector('.sticky-cart-bar .cart-total-amount');
if (stickyCartTotal) {
stickyCartTotal.textContent = `€${total.toFixed(2)}`;
}

// Add click handlers for remove buttons
document.querySelectorAll('.remove-item').forEach(button => {
button.addEventListener('click', function() {
const type = this.dataset.type;
console.log('Remove button clicked:', { type, dataset: this.dataset });

if (type === 'ticket') {
const section = this.dataset.section;
const ticketType = this.dataset.ticketType;

console.log('Attempting to remove ticket:', { section, ticketType });

// Try to find the quantity input with the exact ticket type
let quantityInput = document.querySelector(`.quantity-value[data-section="${section}"][data-ticket-type="${ticketType}"]`);

// If not found, try case-insensitive search
if (!quantityInput) {
const allQuantityInputs = document.querySelectorAll('.quantity-value[data-section="' + section + '"]');
console.log('All quantity inputs for section', section, ':', allQuantityInputs);

// Try to find by exact match first, then case-insensitive
for (let input of allQuantityInputs) {
const inputTicketType = input.dataset.ticketType;
if (inputTicketType === ticketType ||
(inputTicketType && inputTicketType.toLowerCase() === ticketType.toLowerCase())) {
quantityInput = input;
console.log('Found quantity input:', inputTicketType);
break;
}
}
}

if (quantityInput) {
console.log('Found quantity input, setting to 0:', quantityInput);
quantityInput.value = '0';
updateTicketQuantity(section, ticketType, parseFloat(quantityInput.dataset.price), 0);
} else {
console.error('Could not find quantity input for ticket removal:', { section, ticketType });

// Fallback: manually remove from orderData
const ticketIndex = orderData.tickets.findIndex(t =>
t.section === section &&
(t.type === ticketType || t.type.toLowerCase() === ticketType.toLowerCase())
);

if (ticketIndex !== -1) {
console.log('Removing ticket from orderData directly:', orderData.tickets[ticketIndex]);
orderData.tickets.splice(ticketIndex, 1);
updateOrderSummary();
} else {
console.error('Could not find ticket in orderData to remove');
// Force refresh the cart display
updateOrderSummary();
}
}
} else if (type === 'addon') {
const name = this.dataset.name;
const quantityInput = document.querySelector(`.add-on-item .quantity-value[data-addon-name="${name}"]`);
if (quantityInput) {
quantityInput.value = '0';
updateAddonQuantity(name, parseFloat(quantityInput.dataset.price), 0);
}
} else if (type === 'voucher') {
// Clear voucher
const voucherInput = document.querySelector('input[name="voucher"]');
const warningVoucher = document.querySelector('.warning-voucher');
const successVoucher = document.querySelector('.success-voucher');
if (voucherInput) voucherInput.value = '';
if (warningVoucher) warningVoucher.style.display = 'none';
if (successVoucher) successVoucher.style.display = 'none';
updateOrderSummary();
}
});
});

// Only save order data if shouldSave is true
if (shouldSave) {
saveOrderData({
tickets: ticketData || [],
addOns: orderData.addOns || [],
total: total
});
}
}

function addRemoveListenersToSummary() {
document.querySelectorAll('.remove-item').forEach(el => {
el.addEventListener('click', function() {
const type = this.getAttribute('data-type');
const name = this.getAttribute('data-name');
if (type === 'addon') {
updateAddonQuantity(name, 0, 0);
// Update the UI input if present
const input = document.querySelector(`.quantity-value[data-addon-name="${name}"]`);
if (input) input.value = 0;
} else if (type === 'ticket') {
const section = this.getAttribute('data-section');
const ticketType = this.getAttribute('data-ticket-type');
const input = document.querySelector(`.quantity-value[data-section="${section}"][data-ticket-type="${ticketType.toLowerCase()}"]`);
if (input) {
input.value = 0;
input.dispatchEvent(new Event('change', { bubbles: true }));
}
}
});
});
}

function getUrlParams() {
const params = new URLSearchParams(window.location.search);
return {
event_id: params.get('event_id'),
event_date_id: params.get('event_date_id')
};
}

async function fetchEventData(event_id, event_date_id) {
console.log('Starting fetchEventData…');
console.log('URL parameters:', { event_id, event_date_id });

try {
const url = `/api/events.php?event_id=${event_id}&event_date_id=${event_date_id}`;
console.log('Making request to:', url);

const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
cache: 'no-cache'
});

console.log('Response status:', response.status);

if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}

const data = await response.json();
console.log('API response:', data);

if (data.error) {
throw new Error(data.error);
}

// Find the specific event date
const eventDate = data.event_dates.find(date => date.event_date_id === parseInt(event_date_id));
console.log('Found event date:', eventDate);

if (!eventDate) {
throw new Error('Event date not found');
}

// Check if showtime is active
if (eventDate.showtime_status !== 'active') {
throw new Error('This showtime is not available for booking');
}

// Check if booking is closed (within 30 minutes of showtime)
if (eventDate.booking_closed) {
// Calculate minutes until showtime
const showtime = new Date(eventDate.event_datetime);
const now = new Date();
const timeDiff = showtime – now;
const minutesUntilShow = Math.floor(timeDiff / (1000 * 60));

// Show online closed message with countdown
const onlineClosedMessage = `ONLINE CLOSED – Starts in ${minutesUntilShow} min`;

// Update the page to show online closed message
document.body.innerHTML = `

${onlineClosedMessage}

Go to Boxoffice

Return to Home

`;
return; // Stop further processing
}

// Format the date string
const dateString = new Date(eventDate.event_datetime).toLocaleString('en-GB', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
console.log('Formatted date string:', dateString);

// Store event date string globally
selectedEventDateString = dateString;

// Update venue info in DOM
const venueTitle = document.getElementById('venue-title');
const venueFull = document.getElementById('venue-full');
const eventDateElement = document.getElementById('event-date');
const summaryEventDate = document.getElementById('summary-event-date');

if (venueTitle) venueTitle.textContent = data.event.venue_title;
if (venueFull) venueFull.textContent = data.event.venue_full;
if (eventDateElement) eventDateElement.textContent = dateString;
if (summaryEventDate) summaryEventDate.textContent = dateString;

// Update order info block
const orderInfoBlock = document.querySelector('.order-info-block');
if (orderInfoBlock) {
orderInfoBlock.innerHTML = `
Order Information

${dateString}

${data.event.venue_title}

${data.event.venue_full}

`;
}

// Fetch dynamic add-ons
let addOns = [];
try {
const addonsResponse = await fetch('https://boxoffice.fossettscircus.com/api/addons', {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
cache: 'no-cache'
});

if (addonsResponse.ok) {
const contentType = addonsResponse.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const addonsData = await addonsResponse.json();
addOns = addonsData.map(addon => ({
id: addon.id,
name: addon.name.toLowerCase().replace(/s+/g, ''),
label: addon.name,
description: addon.description || '',
price: parseFloat(addon.price),
img: addon.thumb || '/elements/fossetts-popcorn.jpg'
}));
}
}
} catch (e) {
console.warn('Error fetching add-ons:', e);
}

// Store add-ons data globally for label lookup
globalAddOnsData = addOns;
console.log('Global add-ons data stored:', globalAddOnsData);

// Process the new data structure with capacities and ticket_definitions
const tickets = [];

// Group ticket definitions by seating area
const ticketGroups = {};
eventDate.ticket_definitions.forEach(ticketDef => {
if (!ticketGroups[ticketDef.seating_area]) {
ticketGroups[ticketDef.seating_area] = [];
}
ticketGroups[ticketDef.seating_area].push(ticketDef);
});

// Create ticket sections for each seating area
Object.keys(ticketGroups).forEach(seatingArea => {
const ticketDefs = ticketGroups[seatingArea];
const capacity = eventDate.capacities.find(cap => cap.seating_area === seatingArea);

console.log('Processing seating area:', seatingArea);
console.log('Ticket definitions for this area:', ticketDefs);

tickets.push({
title: seatingArea,
css_class: seatingArea.toLowerCase().replace(/s+/g, '-'),
seating_type: 'Non allocated seating',
capacity: capacity ? capacity.capacity : 0,
ticket_types: ticketDefs.map(ticketDef => {
console.log('Processing ticket definition:', ticketDef);
return {
label: ticketDef.name,
price: parseFloat(ticketDef.price),
select_name: `event_seat_${ticketDef.name.toLowerCase().replace(/s+/g, '_')}`,
seats_per_ticket: ticketDef.seats_per_ticket || 1
};
})
});
});

console.log('Processed tickets:', tickets);

return {
venue_title: data.event.venue_title || '',
venue_full: data.event.venue_full || '',
event_date_string: dateString,
tickets: tickets,
add_ons: addOns
};
} catch (error) {
console.error('Error in fetchEventData:', error);
throw error;
}
}

function createTicketType(ticket) {
const ticketType = document.createElement('div');
ticketType.className = 'ticket-type';
ticketType.innerHTML = `
${ticket.ticket_type}


0
+

`;

// Add event listeners for plus and minus buttons
const minusBtn = ticketType.querySelector('.minus');
const plusBtn = ticketType.querySelector('.plus');
const quantityValue = ticketType.querySelector('.quantity-value');

minusBtn.addEventListener('click', () => {
const currentValue = parseInt(quantityValue.textContent);
if (currentValue > 0) {
quantityValue.textContent = currentValue – 1;
updateTicketQuantity(ticket.ticket_id, currentValue – 1);
}
});

plusBtn.addEventListener('click', () => {
const currentValue = parseInt(quantityValue.textContent);
if (currentValue < ticket.seats_left) {
quantityValue.textContent = currentValue + 1;
updateTicketQuantity(ticket.ticket_id, currentValue + 1);
}
});

return ticketType;
}

function updateTicketQuantity(section, ticketType, price, quantity) {
// Normalize ticket type to uppercase for consistency, but preserve original for display
const normalizedTicketType = ticketType.toUpperCase();

// Find or create ticket in orderData
const ticketIndex = orderData.tickets.findIndex(t =>
t.section === section && t.type === normalizedTicketType
);

if (ticketIndex === -1) {
// Add new ticket
orderData.tickets.push({
section: section,
type: normalizedTicketType,
price: price,
quantity: quantity
});
} else {
// Update existing ticket
orderData.tickets[ticketIndex].quantity = quantity;
}

console.log('Updated ticket quantity:', {
section: section,
ticketType: ticketType,
normalizedType: normalizedTicketType,
price: price,
quantity: quantity,
orderData: orderData.tickets
});

// Update cart display
updateOrderSummary();
}
and give me updated file

Assistant
Thanks! Based on your existing code and the fact that you're using a custom `order.php` to handle ticket quantities, add-ons, and voucher logic, here’s the **exact refactored structure** to fully support **Stripe Hosted Checkout**.

## ✅ Refactored `order.php` for Stripe Checkout

This version:
– Collects ticket + add-on data
– Calculates total price
– Generates line items
– Creates a Checkout Session
– Returns `sessionId` to frontend for redirect

### 🔧 `order.php`

“`php

User
what will first execute buy-tickets.js or order.php
Assistant
Here’s how the flow works:

### ❓ Which Executes First — `buy-tickets.js` or `order.php`?

It depends on **who triggers the flow**, but in your new **Stripe Hosted Checkout setup**, the order is:

### ✅ 1. `buy-tickets.js` (Client-side JavaScript) executes first
– When the user clicks the **"Buy Tickets"** or **"Checkout"** button,
– `buy-tickets.js` collects the form data (tickets, add-ons, voucher, etc.)
– Sends a `POST` request to `order.php` via `fetch()` or `XMLHttpRequest`.

fetch('/order.php', {
  method: 'POST',
  body: formData
});

### ✅ 2. `order.php` (Server-side PHP) executes next
– PHP receives the request
– Parses the form data
– Builds the Stripe Checkout Session with line items
– Returns a JSON response with the `sessionId`

echo json_encode(['sessionId' => $session->id]);

### ✅ 3. Then `buy-tickets.js` continues
– It reads the `sessionId` from the response
– Calls `stripe.redirectToCheckout({ sessionId })` to send user to Stripe’s hosted payment page.

### 🧠 Summary Flow

| Step | Action | Location |
|——|—————————|—————|
| 1 | User clicks checkout | Frontend JS (`buy-tickets.js`) |
| 2 | JS sends form to PHP | Backend (`order.php`) |
| 3 | PHP creates Stripe session | Backend |
| 4 | Returns `sessionId` | Backend → JS |
| 5 | JS redirects to Stripe | Frontend |

Let me know if you want help **modifying your `buy-tickets.js`** to call the refactored `order.php` and redirect the user correctly.

User
so first give buy-ticker.js updated code and then I will sahe order.php exstiong
Assistant
Perfect. Here's your **updated `buy-tickets.js`** to support **Stripe Hosted Checkout** via `order.php`.

### ✅ `buy-tickets.js` – Updated for Stripe Hosted Checkout

This script:
– Gathers tickets, add-ons, and voucher
– Sends it to `order.php`
– On success, redirects to Stripe

document.addEventListener('DOMContentLoaded', function () {
  const checkoutBtn = document.getElementById('checkout-button');

  if (!checkoutBtn) return;

  checkoutBtn.addEventListener('click', async function (e) {
    e.preventDefault();

    const form = document.getElementById('event-order-form');
    const formData = new FormData(form);

    // Collect wheelchair user data (optional)
    const wheelchairUsers = getWheelchairUserData?.();
    if (wheelchairUsers > 0) {
      formData.append('wheelchair_users', wheelchairUsers);
    }

    // Get event and date IDs from URL (optional – fallback)
    const urlParams = new URLSearchParams(window.location.search);
    const eventId = urlParams.get('event_id');
    const eventDateId = urlParams.get('event_date_id');

    if (eventId) formData.append('event_id', eventId);
    if (eventDateId) formData.append('event_date_id', eventDateId);

    // Append customer name/email/phone if available
    const customerName = document.querySelector('input[name="customer_name"]');
    const customerEmail = document.querySelector('input[name="customer_email"]');
    const customerPhone = document.querySelector('input[name="customer_phone"]');

    if (customerName) formData.append('customer_name', customerName.value);
    if (customerEmail) formData.append('customer_email', customerEmail.value);
    if (customerPhone) formData.append('customer_phone', customerPhone.value);

    try {
      // Send form data to order.php to create Checkout Session
      const response = await fetch('/order.php', {
        method: 'POST',
        body: formData
      });

      const result = await response.json();

      if (result.success && result.sessionId) {
        // Redirect to Stripe Checkout
        const stripe = Stripe('pk_live_YOUR_PUBLIC_KEY'); // Replace with your real key
        await stripe.redirectToCheckout({ sessionId: result.sessionId });
      } else {
        alert(result.message || 'Something went wrong. Please try again.');
      }
    } catch (error) {
      console.error('Error during Stripe checkout:', error);
      alert('Could not process the checkout. Please try again later.');
    }
  });
});

### ✅ Assumptions:

– Your checkout button has the ID: `checkout-button`
– Your form has the ID: `event-order-form`
– `order.php` returns:

{ "success": true, "sessionId": "cs_test_..." }

### 🔄 Next Step

Please share your current `order.php`, and I’ll update it to match this JS and fully support Stripe Hosted Checkout with all existing ticket + add-on logic.

User
<script>
window.location='https://fossettscircus.com/success/?eid=" . $order_info->event_date_id . "&oc=" . $order_info->order_code . "';
</script>

' ";
goto continue_with_wordpress;
exit();
}
else
{
sendErrorMail( "Order not saved", "Order not saved but charge paid" . var_export( $order_info, true ) );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Order could not be saved'
]);
exit;
}
}
}
elseif ($intent->status == 'processing') {
echo json_encode([
'success' => false,
'message' => 'Payment is processing. Please wait a moment and try again.',
'status' => 'processing'
]);
exit;
} elseif ($intent->status == 'requires_payment_method') {
echo json_encode([
'success' => false,
'message' => 'Payment method failed. Please try again with a different card.',
'status' => 'requires_payment_method'
]);
exit;
} elseif ($intent->status == 'requires_confirmation') {
echo json_encode([
'success' => false,
'message' => 'Payment requires confirmation. Please try again.',
'status' => 'requires_confirmation'
]);
exit;
} else {
echo json_encode([
'success' => false,
'message' => 'Payment failed: ' . $intent->status,
'status' => $intent->status,
'errors' => ['There was an error during the transaction, charge was not paid.']
]);
exit;
}
}
catch(StripeErrorCard $e)
{
$body = $e->getJsonBody();
$err = $body['error'];

array_push( $order_errors, $err['message'] );

if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => $err['message'],
'errors' => $order_errors
]);
exit;
}
}
catch (Exception $e)
{
if ( is_object( $e ) && method_exists( $e, 'getJsonBody' ) )
{
$body = $e->getJsonBody();
$err = $body['error'];
array_push( $order_errors, $err['message'] );
}

array_push( $order_errors, 'There was an error during the transaction. Payment not performed.' );

sendErrorMail( "Exception", var_export( $e, true ) );

if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Payment processing error',
'errors' => $order_errors
]);
exit;
}
}
} // /Process payment

} // /Form submitted

?>

<style>
<?php echo file_get_contents( __DIR__ . '/css/style.css'); ?>
/*.div-wrapper{
    padding: 0em 1em !important;
}*/
.payment-fields {
    display: block !important;
    padding: 1em !important;
    border-radius: 5px;
    width: 100%;
    height: 3em;
    color: rgb(66, 109, 143);
    background-color: rgb(255,255,255);
    font-weight: bold;
}
</style>
<script>
<?php echo file_get_contents( __DIR__ . '/js/hashes.js'); ?>
</script>
<script>

</script>
<script>
var event_seat_location_options;
    var eventData = <?php echo json_encode($event_data); ?>;

    jQuery(document).ready(function() {
        if (!eventData || !eventData.event_dates) {
            console.error('Event data not available');
            return;
        }

        var latest_available_seat = jQuery('#event-order [name="event_seat_location"] option:selected').val();
        event_seat_location_options = jQuery('#event-order [name="event_seat_location"]').html();
        jQuery('#event-order [name="event_seat_location"]').html('<option value="">Pick a seat location</option>');
        updateAvailableLocations();
        jQuery('#event-order [name="event_seat_location"]').val(latest_available_seat);
    });
    
    function updateFormSent()
    {
        var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
        jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').removeAttr('disabled');
        jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').show();
        updateTotal();
    }

    function updateAvailableLocations() {
        if (!eventData || !eventData.event_dates) {
            console.error('Event data not available');
            return;
        }

        // reset options
        jQuery('#event-order [name="event_seat_location"]').html(event_seat_location_options);

        var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
        
        jQuery('#event-order [name="event_seat_location"] option').prop('disabled', true).hide();
        jQuery('#event-order [name="event_seat_location"] option:first').prop('disabled', false).show();
        jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').prop('disabled', false).show();
        
        // remove unwanted options
        jQuery('#event-order [name="event_seat_location"]').find('option:disabled').remove();

        jQuery('#event-order [name="event_seat_location"]').val('');
    }

    function updateAvailableSeats()
    {
        var seatsLeft = parseInt( jQuery('#event-order [name="event_seat_location"] option:selected').attr('data-left') );

        var seatsSelected = parseInt( jQuery('#event-order [name="event_seat_full"]').val() ) + parseInt( jQuery("#event-order [name='event_seat_child']").val() ) + parseInt( jQuery("#event-order [name='event_seat_senior']").val() );
    
        if ( seatsSelected > seatsLeft )
        {
            jQuery('#event-order .warning-seats').html('You selected more tickets than are available right now in ' + jQuery("#event-order [name='event_seat_location'] option:selected").text() + ', please pick a different location or reduce the number of tickets by ' + ( seatsSelected - seatsLeft ) + '.' );
            jQuery('#event-order .warning-seats').show();
            return false;
        }
        else
        {
            jQuery('#event-order .warning-seats').hide();
            jQuery('#event-order .warning-seats').html('');
            return true;
        }
    }

    var V = JSON.parse( '<?php echo $event_data->event_vouchers; ?>' );
    var D = 0;
    var T = '';

    function voucherValid()
    {
        var SHA1 = new Hashes.SHA1;

        var customer_voucher = SHA1.hex( jQuery("#event-order [name='voucher']").val().toLowerCase() );

        for (var i = 0; i < V.length; i++)
        {
            if ( customer_voucher == V[i].voucher_code )
            {
                D = V[i].voucher_discount;
                T = V[i].voucher_discount_type;
                return true;
            }
        }

        return false;
    }

    function updateTotal()
    {
        var seatsFull = parseInt( jQuery('#event-order [name="event_seat_full"]').val() );
        var seatsChild = parseInt( jQuery('#event-order [name="event_seat_child"]').val() );
        var seatsSenior = parseInt( jQuery('#event-order [name="event_seat_senior"]').val() );
    
        var priceFull = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-full' ) );
        var priceReduced = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-reduced' ) );
    
        var total = ( seatsFull * priceFull ) + ( ( seatsChild + seatsSenior ) * priceReduced );

        if ( voucherValid() )
        {
            if ( T == 'percentage' )
            {
                total = total * ( 1 - ( D / 100 ) );
            }
            else if ( T == 'amount' )
            {
                total = total - D;
            }
        }
        
        var currency = '<?php echo $currency_symbol; ?>';

        if ( isNaN( total ) )
        {
            jQuery('#event-order .total').html('');
        }
        else
        {
            jQuery('#event-order .total').html('Total: '+ currency + total );
        }
    }

    jQuery(document).ready(function()
    {
        /*var stripe = Stripe('pk_test_VBObL72a6bVf4DAEpzOKvlh1', { // thenet
           stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
        });*/
        
        var stripe = Stripe('pk_live_KSh8MRi76Kliuk4fUpBZ23GS', { // thenet live mode
            stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
        });
        
    <?php if ( isset( $_POST['payment_intent_client_secret'] ) ): ?>
    
        stripe.handleCardAction(
            '<?php echo $_POST['payment_intent_client_secret']; ?>'
        ).then(function(result)
        {
            if (result.error)
            {
                jQuery('#event-order .warning-payment').html( result.error.message );
                jQuery('#event-order .warning-payment').show();
            }
            else
            {
                jQuery('[name="payment_intent_id"]').val( result.paymentIntent.id  );
                jQuery('#event-order .warning-payment').hide();
                jQuery('#event-order .warning-payment').html( '' );
                jQuery("#event-order #event-order-form").trigger('submit');
            }
        });

    <?php else: ?>

        var elements = stripe.elements();

        var paymentStyle = {
            base: {
                color: 'rgb(66, 109, 143)',
                '::placeholder': {
                    color: 'rgba(66, 109, 143, 0.7)',
                },
            }
        };

        // var card = elements.create('card', { hidePostalCode: true, style: paymentStyle });
        var card = elements.create('cardNumber', { showIcon: true, style: paymentStyle });
        var cardExpiry = elements.create('cardExpiry', { style: paymentStyle });
        var cardCvc = elements.create('cardCvc', { style: paymentStyle });

        card.addEventListener('change', function( event )
        {
            if ( event.error )
            {
                jQuery('#event-order .warning-payment').html( event.error.message );
                jQuery('#event-order .warning-payment').show();
            }
            else
            {
                jQuery('#event-order .warning-payment').hide();
                jQuery('#event-order .warning-payment').html( '' );
            }
        });

        // card.mount('#event-order #payment-fields');
        
        card.mount('#event-order #card-number');
        cardExpiry.mount('#event-order #card-expiry');
        cardCvc.mount('#event-order #card-cvc');
        
    <?php endif; ?>

        jQuery("#event-order [name='event_datetime']").on("change", function()
        {
            updateAvailableLocations();
            updateTotal();
        });

        jQuery("#event-order [name='event_seat_location'], #event-order [name='event_seat_full'], #event-order [name='event_seat_child'], #event-order [name='event_seat_senior']").on("change", function()
        {
            updateAvailableSeats();
            updateTotal();
        });

        jQuery("#event-order [name='voucher']").on('input',function()
        {
            if ( voucherValid() )
            {
                jQuery('.warning-voucher').hide();
                jQuery('.success-voucher').show();
            }
            else
            {
                if ( jQuery(this).val() == '' )
                {
                    jQuery('.success-voucher').hide();
                    jQuery('.warning-voucher').hide();
                }
                else
                {
                    jQuery('.success-voucher').hide();
                    jQuery('.warning-voucher').show();
                }
            }

            updateTotal();
        });

        jQuery("#event-order .order-submit").on('click', function()
        {
            jQuery("#event-order .order-submit").prop('disabled', true);

            if ( updateAvailableSeats() )
            {
                // Stripe SCA
                stripe.createPaymentMethod('card', card, {
                    billing_details: { name: jQuery('[name="customer_name"]').val() }
                })
                //stripe.createToken( card )
                .then( function(result)
                {
                    if ( result.error )
                    {
                        jQuery('#event-order .warning-payment').html( result.error.message );
                        jQuery('#event-order .warning-payment').show();
                        jQuery("#event-order .order-submit").prop('disabled', false);
                    }
                    else
                    {
                        jQuery('#event-order [name="payment_token"]').val( result.paymentMethod.id );
                        //jQuery('#event-order [name="payment_token"]').val( result.token.id );
                        jQuery('#event-order .warning-payment').hide();
                        jQuery('#event-order .warning-payment').html( '' );
                        jQuery("#event-order #event-order-form").trigger('submit');
                    }
                });
            }
            else
            {
                jQuery("#event-order .order-submit").prop('disabled', false);
            }
        });
        
        /*jQuery("#event-order #event-order-form").on("submit", function(e)
        {
            jQuery("#event-order .order-submit").trigger('click');
            e.preventDefault();
        });*/

        updateFormSent();
        
    });
</script>
<script>
jQuery(document).ready(function()
            {
                jQuery('html, body').animate(
                {
                    scrollTop: ( jQuery('.form-errors').first().offset().top - 10 )
                }, 500 );
            });
</script>
<script>
jQuery(document).ready(function()
        {
            function checkEmailValue(value, arr) {
                var status = 'not_exist';

                for (var i = 0; i < arr.length; i++) {
                    var name = arr[i];
                    if (name == value) {
                        status = 'exist';
                break;
            }
        }

                return status;
            }

            //code to check if a value exists in an array
            var email_pool = ['gmail.com','yahoo.com','hotmail.com','aol.com','hotmail.co.uk','hotmail.fr','msn.com','yahoo.fr','wanadoo.fr','orange.fr','comcast.net','yahoo.co.uk','yahoo.com.br','yahoo.co.in','live.com','rediffmail.com','free.fr','gmx.de','web.de','yandex.ru','ymail.com','libero.it','outlook.com','uol.com.br','bol.com.br','mail.ru','cox.net','hotmail.it','sbcglobal.net','sfr.fr','live.fr','verizon.net','live.co.uk','googlemail.com','yahoo.es','ig.com.br','live.nl','bigpond.com','terra.com.br','yahoo.it','neuf.fr','yahoo.de','alice.it','rocketmail.com','att.net','laposte.net','facebook.com','bellsouth.net','yahoo.in','hotmail.es','charter.net','yahoo.ca','yahoo.com.au','rambler.ru','hotmail.de','tiscali.it','shaw.ca','yahoo.co.jp','sky.com','earthlink.net','optonline.net','freenet.de','t-online.de','aliceadsl.fr','virgilio.it','home.nl','qq.com','telenet.be','me.com','yahoo.com.ar','tiscali.co.uk','yahoo.com.mx','voila.fr','gmx.net','mail.com','planet.nl','tin.it','live.it','ntlworld.com','arcor.de','yahoo.co.id','frontiernet.net','hetnet.nl','live.com.au','yahoo.com.sg','zonnet.nl','club-internet.fr','juno.com','optusnet.com.au','blueyonder.co.uk','bluewin.ch','skynet.be','sympatico.ca','windstream.net','mac.com','centurytel.net','chello.nl','live.ca','aim.com','bigpond.net.au'];

            jQuery( "#confirm_email_address" ).focusout(function() {

                jQuery("#confirm_email_error").css("display", (jQuery("#email_address").val() != jQuery("#confirm_email_address").val()) ? "inline-block" : "none");

                var user_email = jQuery("#email_address").val();

                var domain_name = user_email.indexOf("@");

                var slice_email = user_email.slice((domain_name + 1), user_email.length);

                if(jQuery("#email_address").val() == jQuery("#confirm_email_address").val()){
                    // if(!jQuery.inArray(slice_email, email_pool)){
                    if(checkEmailValue(slice_email, email_pool) == 'not_exist'){
                        jQuery("#confirm_email_error2").css("display", "inline-block");
                        jQuery("#confirm_email_error2").html("Hmmm, are you sure... <strong>"+user_email+"</strong> is the correct email address?");
                    }else{
                        jQuery("#confirm_email_error2").css("display", "none");
                    }
                }
            });

                    
        });
</script>
<style>
.email_address_error{
            margin-top: 5px !important;
            color: rgb(255, 255, 255);
            background-color: rgb(237, 30, 38);
            font-weight: normal;
            padding: 0.3em 15px;
        }.email_address_warning{
            margin-top: 5px !important;
            color: rgb(255, 255, 255);
            background-color: #25a828;
            font-weight: normal;
            padding: 0.3em 15px;
        }
</style>

Contact details

Assistant
Thanks for sharing your `order.php`. It’s currently handling **order creation and email sending directly**, and **manually charging cards via Stripe Elements** — which you now want to **replace with Stripe Hosted Checkout**.

### ✅ Goal

Refactor `order.php` to:
1. **Receive ticket, customer, and add-on data**
2. **Build a Stripe Checkout Session**
3. **Return a `sessionId` to the frontend**
4. Let Stripe handle payment, then **redirect user to success page**

### ✅ Updated `order.php` (Stripe Hosted Checkout)

Here’s a clean version of your `order.php` for Hosted Checkout:

<?php
require 'vendor/autoload.php'; // Make sure Stripe is installed via Composer

StripeStripe::setApiKey('sk_live_...'); // Replace with your actual secret key

header('Content-Type: application/json');

// Read and sanitize inputs
$eventId = $_POST['event_id'] ?? '';
$eventDateId = $_POST['event_date_id'] ?? '';
$customerName = $_POST['customer_name'] ?? '';
$customerEmail = $_POST['customer_email'] ?? '';
$customerPhone = $_POST['customer_phone'] ?? '';
$voucherCode = $_POST['voucher_code'] ?? '';
$wheelchairUsers = (int) ($_POST['wheelchair_users'] ?? 0);

// Validate
if (!$eventId || !$eventDateId || !$customerEmail) {
    echo json_encode(['success' => false, 'message' => 'Missing required fields']);
    exit;
}

// === 1. Build line items from ticket types ===
$lineItems = [];

// Extract all `event_seat_*` fields (example: event_seat_ring_vip => 2)
foreach ($_POST as $key => $value) {
    if (strpos($key, 'event_seat_') === 0 && (int) $value > 0) {
        $label = ucwords(str_replace('_', ' ', substr($key, 11)));
        $lineItems[] = [
            'price_data' => [
                'currency' => 'eur',
                'unit_amount' => 1000, // Replace with actual price lookup logic (in cents)
                'product_data' => [
                    'name' => "Ticket: {$label}"
                ],
            ],
            'quantity' => (int) $value
        ];
    }
}

// === 2. Add add-ons ===
foreach ($_POST as $key => $value) {
    if (strpos($key, 'addon_') === 0 && (int) $value > 0) {
        $addonName = ucfirst(str_replace('_', ' ', $key));
        $lineItems[] = [
            'price_data' => [
                'currency' => 'eur',
                'unit_amount' => 500, // Replace with actual add-on price (in cents)
                'product_data' => [
                    'name' => "Addon: {$addonName}"
                ],
            ],
            'quantity' => (int) $value
        ];
    }
}

// === 3. Add wheelchair users (if needed) ===
if ($wheelchairUsers > 0) {
    $lineItems[] = [
        'price_data' => [
            'currency' => 'eur',
            'unit_amount' => 0,
            'product_data' => [
                'name' => 'Wheelchair Space',
            ]
        ],
        'quantity' => $wheelchairUsers
    ];
}

// === 4. Voucher logic (optional flat discount simulation) ===
$discountAmount = 0;
if ($voucherCode === 'FOSSETTS10') {
    $discountAmount = 1000; // 10 EUR discount
}

// === 5. Create Checkout Session ===
try {
    $checkoutData = [
        'payment_method_types' => ['card'],
        'line_items' => $lineItems,
        'mode' => 'payment',
        'customer_email' => $customerEmail,
        'success_url' => 'https://yourdomain.com/success.php?session_id={CHECKOUT_SESSION_ID}',
        'cancel_url' => 'https://yourdomain.com/cancel.php',
        'metadata' => [
            'event_id' => $eventId,
            'event_date_id' => $eventDateId,
            'customer_name' => $customerName,
            'wheelchair_users' => $wheelchairUsers,
        ]
    ];

    // If discount exists, apply
    if ($discountAmount > 0) {
        $coupon = StripeCoupon::create([
            'amount_off' => $discountAmount,
            'currency' => 'eur',
            'duration' => 'once',
        ]);
        $checkoutData['discounts'] = [[ 'coupon' => $coupon->id ]];
    }

    $session = StripeCheckoutSession::create($checkoutData);

    echo json_encode(['success' => true, 'sessionId' => $session->id]);
} catch (Exception $e) {
    echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}

### 🧩 Integration Tips

– Set `unit_amount` dynamically from your database or pricing logic.
– Save order info **after** successful payment via Stripe webhook.
– Update `success_url` to a confirmation page that retrieves session data:

$session = StripeCheckoutSession::retrieve($_GET['session_id']);

Would you like me to:
– Hook this into your **existing email sending / order recording** system?
– Help set up the **Stripe webhook listener** for `checkout.session.completed`?

Let me know, and I’ll guide you step by step.

User
<script>
window.location='https://fossettscircus.com/success/?eid=" . $order_info->event_date_id . "&oc=" . $order_info->order_code . "';
</script>

' ";
goto continue_with_wordpress;
exit();
}
else
{
sendErrorMail( "Order not saved", "Order not saved but charge paid" . var_export( $order_info, true ) );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Order could not be saved'
]);
exit;
}
}
}
elseif ($intent->status == 'processing') {
echo json_encode([
'success' => false,
'message' => 'Payment is processing. Please wait a moment and try again.',
'status' => 'processing'
]);
exit;
} elseif ($intent->status == 'requires_payment_method') {
echo json_encode([
'success' => false,
'message' => 'Payment method failed. Please try again with a different card.',
'status' => 'requires_payment_method'
]);
exit;
} elseif ($intent->status == 'requires_confirmation') {
echo json_encode([
'success' => false,
'message' => 'Payment requires confirmation. Please try again.',
'status' => 'requires_confirmation'
]);
exit;
} else {
echo json_encode([
'success' => false,
'message' => 'Payment failed: ' . $intent->status,
'status' => $intent->status,
'errors' => ['There was an error during the transaction, charge was not paid.']
]);
exit;
}
}
catch(StripeErrorCard $e)
{
$body = $e->getJsonBody();
$err = $body['error'];

array_push( $order_errors, $err['message'] );

if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => $err['message'],
'errors' => $order_errors
]);
exit;
}
}
catch (Exception $e)
{
if ( is_object( $e ) && method_exists( $e, 'getJsonBody' ) )
{
$body = $e->getJsonBody();
$err = $body['error'];
array_push( $order_errors, $err['message'] );
}

array_push( $order_errors, 'There was an error during the transaction. Payment not performed.' );

sendErrorMail( "Exception", var_export( $e, true ) );

if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Payment processing error',
'errors' => $order_errors
]);
exit;
}
}
} // /Process payment

} // /Form submitted

?>

<style>
<?php echo file_get_contents( __DIR__ . '/css/style.css'); ?>
/*.div-wrapper{
    padding: 0em 1em !important;
}*/
.payment-fields {
    display: block !important;
    padding: 1em !important;
    border-radius: 5px;
    width: 100%;
    height: 3em;
    color: rgb(66, 109, 143);
    background-color: rgb(255,255,255);
    font-weight: bold;
}
</style>
<script>
<?php echo file_get_contents( __DIR__ . '/js/hashes.js'); ?>
</script>
<script>

</script>
<script>
var event_seat_location_options;
    var eventData = <?php echo json_encode($event_data); ?>;

    jQuery(document).ready(function() {
        if (!eventData || !eventData.event_dates) {
            console.error('Event data not available');
            return;
        }

        var latest_available_seat = jQuery('#event-order [name="event_seat_location"] option:selected').val();
        event_seat_location_options = jQuery('#event-order [name="event_seat_location"]').html();
        jQuery('#event-order [name="event_seat_location"]').html('<option value="">Pick a seat location</option>');
        updateAvailableLocations();
        jQuery('#event-order [name="event_seat_location"]').val(latest_available_seat);
    });
    
    function updateFormSent()
    {
        var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
        jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').removeAttr('disabled');
        jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').show();
        updateTotal();
    }

    function updateAvailableLocations() {
        if (!eventData || !eventData.event_dates) {
            console.error('Event data not available');
            return;
        }

        // reset options
        jQuery('#event-order [name="event_seat_location"]').html(event_seat_location_options);

        var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
        
        jQuery('#event-order [name="event_seat_location"] option').prop('disabled', true).hide();
        jQuery('#event-order [name="event_seat_location"] option:first').prop('disabled', false).show();
        jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').prop('disabled', false).show();
        
        // remove unwanted options
        jQuery('#event-order [name="event_seat_location"]').find('option:disabled').remove();

        jQuery('#event-order [name="event_seat_location"]').val('');
    }

    function updateAvailableSeats()
    {
        var seatsLeft = parseInt( jQuery('#event-order [name="event_seat_location"] option:selected').attr('data-left') );

        var seatsSelected = parseInt( jQuery('#event-order [name="event_seat_full"]').val() ) + parseInt( jQuery("#event-order [name='event_seat_child']").val() ) + parseInt( jQuery("#event-order [name='event_seat_senior']").val() );
    
        if ( seatsSelected > seatsLeft )
        {
            jQuery('#event-order .warning-seats').html('You selected more tickets than are available right now in ' + jQuery("#event-order [name='event_seat_location'] option:selected").text() + ', please pick a different location or reduce the number of tickets by ' + ( seatsSelected - seatsLeft ) + '.' );
            jQuery('#event-order .warning-seats').show();
            return false;
        }
        else
        {
            jQuery('#event-order .warning-seats').hide();
            jQuery('#event-order .warning-seats').html('');
            return true;
        }
    }

    var V = JSON.parse( '<?php echo $event_data->event_vouchers; ?>' );
    var D = 0;
    var T = '';

    function voucherValid()
    {
        var SHA1 = new Hashes.SHA1;

        var customer_voucher = SHA1.hex( jQuery("#event-order [name='voucher']").val().toLowerCase() );

        for (var i = 0; i < V.length; i++)
        {
            if ( customer_voucher == V[i].voucher_code )
            {
                D = V[i].voucher_discount;
                T = V[i].voucher_discount_type;
                return true;
            }
        }

        return false;
    }

    function updateTotal()
    {
        var seatsFull = parseInt( jQuery('#event-order [name="event_seat_full"]').val() );
        var seatsChild = parseInt( jQuery('#event-order [name="event_seat_child"]').val() );
        var seatsSenior = parseInt( jQuery('#event-order [name="event_seat_senior"]').val() );
    
        var priceFull = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-full' ) );
        var priceReduced = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-reduced' ) );
    
        var total = ( seatsFull * priceFull ) + ( ( seatsChild + seatsSenior ) * priceReduced );

        if ( voucherValid() )
        {
            if ( T == 'percentage' )
            {
                total = total * ( 1 - ( D / 100 ) );
            }
            else if ( T == 'amount' )
            {
                total = total - D;
            }
        }
        
        var currency = '<?php echo $currency_symbol; ?>';

        if ( isNaN( total ) )
        {
            jQuery('#event-order .total').html('');
        }
        else
        {
            jQuery('#event-order .total').html('Total: '+ currency + total );
        }
    }

    jQuery(document).ready(function()
    {
        /*var stripe = Stripe('pk_test_VBObL72a6bVf4DAEpzOKvlh1', { // thenet
           stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
        });*/
        
        var stripe = Stripe('pk_live_KSh8MRi76Kliuk4fUpBZ23GS', { // thenet live mode
            stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
        });
        
    <?php if ( isset( $_POST['payment_intent_client_secret'] ) ): ?>
    
        stripe.handleCardAction(
            '<?php echo $_POST['payment_intent_client_secret']; ?>'
        ).then(function(result)
        {
            if (result.error)
            {
                jQuery('#event-order .warning-payment').html( result.error.message );
                jQuery('#event-order .warning-payment').show();
            }
            else
            {
                jQuery('[name="payment_intent_id"]').val( result.paymentIntent.id  );
                jQuery('#event-order .warning-payment').hide();
                jQuery('#event-order .warning-payment').html( '' );
                jQuery("#event-order #event-order-form").trigger('submit');
            }
        });

    <?php else: ?>

        var elements = stripe.elements();

        var paymentStyle = {
            base: {
                color: 'rgb(66, 109, 143)',
                '::placeholder': {
                    color: 'rgba(66, 109, 143, 0.7)',
                },
            }
        };

        // var card = elements.create('card', { hidePostalCode: true, style: paymentStyle });
        var card = elements.create('cardNumber', { showIcon: true, style: paymentStyle });
        var cardExpiry = elements.create('cardExpiry', { style: paymentStyle });
        var cardCvc = elements.create('cardCvc', { style: paymentStyle });

        card.addEventListener('change', function( event )
        {
            if ( event.error )
            {
                jQuery('#event-order .warning-payment').html( event.error.message );
                jQuery('#event-order .warning-payment').show();
            }
            else
            {
                jQuery('#event-order .warning-payment').hide();
                jQuery('#event-order .warning-payment').html( '' );
            }
        });

        // card.mount('#event-order #payment-fields');
        
        card.mount('#event-order #card-number');
        cardExpiry.mount('#event-order #card-expiry');
        cardCvc.mount('#event-order #card-cvc');
        
    <?php endif; ?>

        jQuery("#event-order [name='event_datetime']").on("change", function()
        {
            updateAvailableLocations();
            updateTotal();
        });

        jQuery("#event-order [name='event_seat_location'], #event-order [name='event_seat_full'], #event-order [name='event_seat_child'], #event-order [name='event_seat_senior']").on("change", function()
        {
            updateAvailableSeats();
            updateTotal();
        });

        jQuery("#event-order [name='voucher']").on('input',function()
        {
            if ( voucherValid() )
            {
                jQuery('.warning-voucher').hide();
                jQuery('.success-voucher').show();
            }
            else
            {
                if ( jQuery(this).val() == '' )
                {
                    jQuery('.success-voucher').hide();
                    jQuery('.warning-voucher').hide();
                }
                else
                {
                    jQuery('.success-voucher').hide();
                    jQuery('.warning-voucher').show();
                }
            }

            updateTotal();
        });

        jQuery("#event-order .order-submit").on('click', function()
        {
            jQuery("#event-order .order-submit").prop('disabled', true);

            if ( updateAvailableSeats() )
            {
                // Stripe SCA
                stripe.createPaymentMethod('card', card, {
                    billing_details: { name: jQuery('[name="customer_name"]').val() }
                })
                //stripe.createToken( card )
                .then( function(result)
                {
                    if ( result.error )
                    {
                        jQuery('#event-order .warning-payment').html( result.error.message );
                        jQuery('#event-order .warning-payment').show();
                        jQuery("#event-order .order-submit").prop('disabled', false);
                    }
                    else
                    {
                        jQuery('#event-order [name="payment_token"]').val( result.paymentMethod.id );
                        //jQuery('#event-order [name="payment_token"]').val( result.token.id );
                        jQuery('#event-order .warning-payment').hide();
                        jQuery('#event-order .warning-payment').html( '' );
                        jQuery("#event-order #event-order-form").trigger('submit');
                    }
                });
            }
            else
            {
                jQuery("#event-order .order-submit").prop('disabled', false);
            }
        });
        
        /*jQuery("#event-order #event-order-form").on("submit", function(e)
        {
            jQuery("#event-order .order-submit").trigger('click');
            e.preventDefault();
        });*/

        updateFormSent();
        
    });
</script>
<script>
jQuery(document).ready(function()
            {
                jQuery('html, body').animate(
                {
                    scrollTop: ( jQuery('.form-errors').first().offset().top - 10 )
                }, 500 );
            });
</script>
<script>
jQuery(document).ready(function()
        {
            function checkEmailValue(value, arr) {
                var status = 'not_exist';

                for (var i = 0; i < arr.length; i++) {
                    var name = arr[i];
                    if (name == value) {
                        status = 'exist';
                break;
            }
        }

                return status;
            }

            //code to check if a value exists in an array
            var email_pool = ['gmail.com','yahoo.com','hotmail.com','aol.com','hotmail.co.uk','hotmail.fr','msn.com','yahoo.fr','wanadoo.fr','orange.fr','comcast.net','yahoo.co.uk','yahoo.com.br','yahoo.co.in','live.com','rediffmail.com','free.fr','gmx.de','web.de','yandex.ru','ymail.com','libero.it','outlook.com','uol.com.br','bol.com.br','mail.ru','cox.net','hotmail.it','sbcglobal.net','sfr.fr','live.fr','verizon.net','live.co.uk','googlemail.com','yahoo.es','ig.com.br','live.nl','bigpond.com','terra.com.br','yahoo.it','neuf.fr','yahoo.de','alice.it','rocketmail.com','att.net','laposte.net','facebook.com','bellsouth.net','yahoo.in','hotmail.es','charter.net','yahoo.ca','yahoo.com.au','rambler.ru','hotmail.de','tiscali.it','shaw.ca','yahoo.co.jp','sky.com','earthlink.net','optonline.net','freenet.de','t-online.de','aliceadsl.fr','virgilio.it','home.nl','qq.com','telenet.be','me.com','yahoo.com.ar','tiscali.co.uk','yahoo.com.mx','voila.fr','gmx.net','mail.com','planet.nl','tin.it','live.it','ntlworld.com','arcor.de','yahoo.co.id','frontiernet.net','hetnet.nl','live.com.au','yahoo.com.sg','zonnet.nl','club-internet.fr','juno.com','optusnet.com.au','blueyonder.co.uk','bluewin.ch','skynet.be','sympatico.ca','windstream.net','mac.com','centurytel.net','chello.nl','live.ca','aim.com','bigpond.net.au'];

            jQuery( "#confirm_email_address" ).focusout(function() {

                jQuery("#confirm_email_error").css("display", (jQuery("#email_address").val() != jQuery("#confirm_email_address").val()) ? "inline-block" : "none");

                var user_email = jQuery("#email_address").val();

                var domain_name = user_email.indexOf("@");

                var slice_email = user_email.slice((domain_name + 1), user_email.length);

                if(jQuery("#email_address").val() == jQuery("#confirm_email_address").val()){
                    // if(!jQuery.inArray(slice_email, email_pool)){
                    if(checkEmailValue(slice_email, email_pool) == 'not_exist'){
                        jQuery("#confirm_email_error2").css("display", "inline-block");
                        jQuery("#confirm_email_error2").html("Hmmm, are you sure... <strong>"+user_email+"</strong> is the correct email address?");
                    }else{
                        jQuery("#confirm_email_error2").css("display", "none");
                    }
                }
            });

                    
        });
</script>
<style>
.email_address_error{
            margin-top: 5px !important;
            color: rgb(255, 255, 255);
            background-color: rgb(237, 30, 38);
            font-weight: normal;
            padding: 0.3em 15px;
        }.email_address_warning{
            margin-top: 5px !important;
            color: rgb(255, 255, 255);
            background-color: #25a828;
            font-weight: normal;
            padding: 0.3em 15px;
        }
</style>

Contact details

Assistant
Thanks for the confirmation. Here’s how to fully transition to **Stripe Hosted Checkout** while still saving the order and sending confirmation emails after successful payment.

## ✅ Strategy: Save Order After Payment (Using Webhooks)

You **should not** send email or save orders inside `order.php`, because:
– User can abandon checkout.
– Payment might fail.
– Stripe guarantees confirmation via `checkout.session.completed`.

## ✅ Step-by-Step Setup

### 1. ✅ `order.php` (only creates Checkout session)

You're already doing this correctly: just send back `sessionId` and let Stripe handle the rest.

> ✅ No need to modify `order.php` further. Keep it clean and simple.

### 2. ✅ Set up Webhook Endpoint

Create a file: `stripe-webhook.php`

<?php
require 'vendor/autoload.php'; // Stripe SDK

StripeStripe::setApiKey('sk_live_...'); // Your secret key

$endpoint_secret = 'whsec_...'; // Webhook signing secret from Stripe dashboard

$payload = @file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';

try {
    $event = StripeWebhook::constructEvent(
        $payload, $sig_header, $endpoint_secret
    );
} catch (UnexpectedValueException $e) {
    http_response_code(400);
    exit('Invalid payload');
} catch (StripeExceptionSignatureVerificationException $e) {
    http_response_code(400);
    exit('Invalid signature');
}

// 🎯 Handle checkout.session.completed
if ($event->type === 'checkout.session.completed') {
    $session = $event->data->object;

    $customerEmail = $session->customer_email;
    $metadata = $session->metadata;

    $eventId = $metadata['event_id'] ?? '';
    $eventDateId = $metadata['event_date_id'] ?? '';
    $wheelchairUsers = $metadata['wheelchair_users'] ?? 0;
    $customerName = $metadata['customer_name'] ?? '';

    // 🧠 Optionally retrieve full line items if needed
    $lineItems = StripeCheckoutSession::allLineItems($session->id, ['limit' => 100]);

    // 📨 Send email confirmation
    mail($customerEmail, "Your Fossetts Ticket", "Thank you, {$customerName}. Your order was successful.");

    // 💾 Save to database (if needed)
    // Example: insert into your orders table
    // saveOrder($session->id, $eventId, $eventDateId, $lineItems, $customerEmail);

    http_response_code(200);
    echo 'Success';
} else {
    http_response_code(200); // Handle other event types if needed
    echo 'Ignored';
}

### 3. ✅ Configure Stripe Dashboard

Go to:
**Developers → Webhooks → Add endpoint**

– URL: `https://yourdomain.com/stripe-webhook.php`
– Events to send:
– `checkout.session.completed`
– (Optional) `payment_intent.payment_failed`

Copy the `Signing secret` and paste into `$endpoint_secret`.

### 4. ✅ Optional: Create Confirmation Page

In your `success.php`:

require 'vendor/autoload.php';
StripeStripe::setApiKey('sk_live_...');

$session = StripeCheckoutSession::retrieve($_GET['session_id']);
echo "<h2>Thank you, " . htmlspecialchars($session->customer_email) . "</h2>";

## ✅ Summary

| File | Purpose |
|———————–|—————————————-|
| `order.php` | Creates Stripe session and returns ID |
| `buy-tickets.js` | Sends data, redirects to Stripe |
| `stripe-webhook.php` | Receives confirmation and saves order |
| `success.php` | Confirms order to user |

Let me know if you want help saving order details to MySQL in `stripe-webhook.php`. I’ll provide the full code.

Leave a Comment

Your email address will not be published. Required fields are marked *