Facebook in-app browser fix
August 25, 2025 · AI Conversations
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_test_516RiT9I5CeFyorHKnzzqo6sPzB0RTZNygaiV21fv7q1qXgktWeZjHre1SqGpYSAMJtuGmtHh1I48Dz1G5IQLYvmQ00Nl16m7Oo', {
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('/boxoffice/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();
}
stripel carddetails can not writable
—
## Quick wins (do these first)
1) **Mount Elements only when visible (Step 3)**
Right now you call `initializeStripe()` on `DOMContentLoaded` **and** again on step 3. Mounting into a hidden container often breaks focus on in‑app browsers.
**Fix:** remove the early call; only init when Step 3 is active.
// REMOVE this (near DOMContentLoaded):
// initializeStripe();
// KEEP this:
if (stepNumber === 3) {
initializeStripe();
}
2) **Don’t touch Stripe’s private DOM**
Your `fixStripeAccessibilityWarnings()` and mutation observer try to modify `.__PrivateStripeElement-input`. In webviews that can make inputs unfocusable.
**Fix:** remove the whole block that edits `aria-hidden`, `pointerEvents`, etc., and the console.warn override.
3) **Avoid transforms/fixed ancestors around the card fields**
Inputs inside cross‑origin iframes often can’t focus on iOS/FB webviews if any ancestor has CSS like `transform`, `filter`, `perspective`, or is `position: fixed` with overlaying siblings.
**Fix:** make the card fields live in a “plain” container:
/* the wrapper that contains #card-number, #card-expiry, #card-cvc */
.card-fields-wrapper,
.card-fields-wrapper * {
transform: none !important;
filter: none !important;
perspective: none !important;
}
.card-fields-wrapper {
position: relative !important; /* not fixed */
overflow: visible !important;
z-index: 1;
}
Also make sure no invisible overlay sits on top (e.g., slide‑out cart, sticky bars). If in doubt, temporarily add:
#card-number, #card-expiry, #card-cvc,
#card-number * , #card-expiry * , #card-cvc * {
pointer-events: auto !important;
}
4) **Mount after the step is actually shown**
If the parent was `display:none` when mounting, re‑mount after you show Step 3:
let cardNumber, cardExpiry, cardCvc;
async function initializeStripe() {
if (stripeInitialized) return;
stripe = Stripe('pk_test_...', { stripeAccount: 'acct_...' });
elements = stripe.elements({ appearance: { theme: 'stripe' } });
cardNumber = elements.create('cardNumber', { placeholder: 'Card number' });
cardExpiry = elements.create('cardExpiry', { placeholder: 'MM/YY' });
cardCvc = elements.create('cardCvc', { placeholder: 'CVC' });
// Mount AFTER the step 3 container is visible
cardNumber.mount('#card-number');
cardExpiry.mount('#card-expiry');
cardCvc.mount('#card-cvc');
stripeInitialized = true;
}
// If Step 3 becomes visible later, and fields look dead, re‑mount:
function remountCardIfNeeded() {
const cn = elements.getElement('cardNumber');
if (!cn) return; // not initialized
// Unmount and mount again if the iframe got borked by WebView
try { cn.unmount(); } catch {}
try { cardExpiry.unmount(); } catch {}
try { cardCvc.unmount(); } catch {}
cardNumber.mount('#card-number');
cardExpiry.mount('#card-expiry');
cardCvc.mount('#card-cvc');
}
if (typeof MutationObserver !== 'undefined') {
const obs = new MutationObserver(() => {
const step3Visible = document.querySelector('.step_3.active');
if (step3Visible) remountCardIfNeeded();
});
obs.observe(document.body, { attributes: true, childList: true, subtree: true });
}
—
## Facebook in‑app specific hardening
5) **Detect in‑app browser & offer “Open in browser”**
Many in‑app browsers block Payment Request, 3DS popups, and sometimes break `iframe` focus. Give users a one‑tap escape hatch.
function isInAppBrowser() {
const ua = navigator.userAgent || '';
return /FBAN|FBAV|FB_IAB|Instagram|Line/|Twitter/i.test(ua);
}
document.addEventListener('DOMContentLoaded', () => {
if (isInAppBrowser()) {
const bar = document.createElement('div');
bar.className = 'iab-warning';
bar.innerHTML = `
⚠️ For a smoother checkout, <a href="#" id="openExternal">open in your browser</a>.
`;
document.body.prepend(bar);
document.getElementById('openExternal').addEventListener('click', (e) => {
e.preventDefault();
// Try to break out of IAB
window.location.href = window.location.href.replace(/^http:/, 'https:');
setTimeout(() => window.open(window.location.href, '_blank', 'noopener'), 50);
});
}
});
.iab-warning {
position: sticky; top: 0; z-index: 9999;
background: #fff3cd; color: #856404;
padding: 10px 14px; font-size: 14px; border-bottom: 1px solid #ffeeba;
}
.iab-warning a { text-decoration: underline; font-weight: 600; }
6) **CSP/Headers for Stripe**
Ensure **server headers** (not “) allow Stripe:
– `script-src` include `https://js.stripe.com`
– `frame-src` (or `child-src`) include `https://js.stripe.com`
– `connect-src` include `https://api.stripe.com`
– `img-src` include `https://*.stripe.com data:`
– Avoid putting `frame-ancestors` in “; set it via server header if needed.
Example Apache:
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.stripe.com; connect-src 'self' https://api.stripe.com; frame-src https://js.stripe.com;"
</IfModule>
7) **No overlay over the iframes**
Your sticky bars / slide‑out cart can overlap. On FB webview even a 1px transparent overlay kills input. Temporarily add a debug background to verify:
.cart-slide-out, .sticky-cart-bar { background: rgba(255,0,0,0.02) !important; }
If you see red over the card boxes, they’re covered. Raise the card block z‑index, or lower/hide overlays on Step 3.
—
## Other polish
– **One source of truth for init**: you already guard with `stripeInitialized`. Keep it, but *don’t* also init on DOMContentLoaded.
– **Parent zoom/font**: set `html { -webkit-text-size-adjust: 100%; }` and `input, button { font-size: 16px; }` to avoid odd zoom/blur behaviors.
– **Payment Request**: you’re hiding it anyway — good. IABs rarely support it.
—
## TL;DR checklist
– [ ] Remove the “fix Stripe accessibility” code & console.warn override.
– [ ] Initialize & mount Elements **only** when Step 3 is visible; if needed, unmount/remount after showing Step 3.
– [ ] Ensure **no ancestor** of the card fields uses `transform`, `filter`, `perspective`, or `position: fixed` overlays.
– [ ] Add CSP headers for Stripe (server‑side), not “.
– [ ] Detect FB/IG in‑app and show an **Open in browser** banner.
If you want, paste me a quick DOM snippet (the HTML around `#card-number/#card-expiry/#card-cvc`) and I’ll mark exactly which container to de‑transform or re‑mount into.