Stripe Facebook issue debug
August 7, 2025 · AI Conversations
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();
}
### 🔍 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?
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
—
### 🧨 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?
<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
—
### ✅ 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.
—
### ✅ 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?
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
—
## ✅ 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?
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
—
## ✅ 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
—
### ❓ 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.
—
### ✅ `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.
<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
—
### ✅ 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.
<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
—
## ✅ 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.