class SquareCheckout {
|
constructor(config = {}) {
|
|
this.checkout = document.querySelector('aside#cart');
|
if (!this.checkout) {
|
return;
|
}
|
|
this.config = Object.assign({
|
application_id: squareConfig.application_id,
|
location_id: squareConfig.location_id,
|
api_url: squareConfig.api_url,
|
nonce: squareConfig.nonce,
|
currency: squareConfig.currency || 'CAD'
|
}, config);
|
|
|
|
this.stepMultiplier = 1;
|
|
this.cache = new window.jvbCache('cart', {TTL: 8.64e+7});
|
this.a11y = window.jvbA11y;
|
|
this.initCart();
|
|
|
this.payments = null;
|
this.card = null;
|
this.isInitialized = false;
|
|
|
this.clickHandler = this.handleClick.bind(this);
|
this.keyHandler = this.handleEscape.bind(this);
|
this.changeHandler = this.handleChange.bind(this);
|
|
|
this.initElements();
|
this.bindEvents();
|
this.init();
|
|
this.toggle.hidden = false;
|
}
|
|
async initCart() {
|
this.cartItems = await this.cache.get('cart') ?? new Map();
|
console.log('cart',this.cartItems);
|
if (this.cartItems.size > 0) {
|
this.notifyRestoredCart();
|
}
|
}
|
handleClick(e) {
|
if (window.targetCheck(e, '.toggle-cart')) {
|
let toggle = window.targetCheck(e, '.toggle-cart');
|
console.log('Toggle found. Toggling cart');
|
this.toggleCart();
|
} else if (window.targetCheck(e, 'button') && window.targetCheck(e, 'div.quantity')) {
|
let quantity = window.targetCheck(e, 'div.quantity');
|
this.handleNumberClick(e, quantity);
|
}else if (window.targetCheck(e, '[data-add-to-cart]')) {
|
let add = window.targetCheck(e, '[data-add-to-cart]');
|
this.handleAddToCart(add);
|
} else if (window.targetCheck(e, '[data-remove-from-cart]')) {
|
let remove = window.targetCheck(e, '[data-remove-from-cart]');
|
this.handleRemoveFromCart(remove);
|
} else if (window.targetCheck(e, '[data-clear-cart]')) {
|
this.clearCart();
|
} else if (this.checkout.classList.contains('expanded') &&
|
!this.checkout.contains(e.target) &&
|
e.target !== this.toggle) {
|
this.closeCart();
|
}
|
}
|
|
handleChange(e, container) {
|
console.log('Checkout change');
|
let input = window.targetCheck(e, '.quantity-input');
|
if (input) {
|
|
let item = e.target.closest('.quantity');
|
let value = input.value;
|
|
if (window.targetCheck(e, '.cart-items')) {
|
let main = document.querySelector(`.menu-section [data-id="${item.dataset.id}"] input`);
|
if (main) {
|
main.value = input.value;
|
}
|
}
|
|
if (value > 0) {
|
this.handleAddToCart(item);
|
} else {
|
this.handleRemoveFromCart(item);
|
}
|
}
|
}
|
|
handleNumberClick(e, container) {
|
console.log(container);
|
e.preventDefault();
|
let change = 0;
|
let action = '';
|
if (e.target.closest('.increase')) {
|
change += 1;
|
} else if (e.target.closest('.decrease')) {
|
change -=1;
|
}
|
if (change !== 0) {
|
let [
|
step,
|
input
|
] = [
|
parseInt(container.dataset.step),
|
container.querySelector('input'),
|
];
|
|
let value = (input.value === '') ? 0 : parseInt(input.value);
|
|
input.value = (value + (step * change * this.stepMultiplier));
|
|
input.dispatchEvent(new Event('change', {bubbles: true}));
|
this.handleNumberLimits(container);
|
}
|
}
|
|
handleNumberLimits(container) {
|
let [
|
min,
|
max,
|
input,
|
increase,
|
decrease
|
] = [
|
container.dataset.min,
|
container.dataset.max,
|
container.querySelector('input'),
|
container.querySelector('.increase'),
|
container.querySelector('.decrease')
|
];
|
let value = parseInt(input.value);
|
if (value < min) {
|
input.value = min;
|
decrease.disabled = true;
|
} else if (value > max) {
|
input.value = max;
|
increase.disabled = false;
|
} else if (increase.disabled) {
|
increase.disabled = false;
|
} else if (decrease.disabled) {
|
decrease.disabled = false;
|
}
|
}
|
|
toggleCart() {
|
if (!this.checkout.classList.contains('expanded')) {
|
this.openCart();
|
} else {
|
this.closeCart();
|
}
|
}
|
openCart(message = 'Opened Cart') {
|
this.checkout.classList.add('expanded');
|
this.toggle.title = 'Hide cart';
|
this.toggle.ariaExpanded = true;
|
this.toggle.querySelector('span').textContent = 'Close Cart';
|
this.a11y.announce(message);
|
this.maybeAddEmptyState();
|
document.addEventListener('keydown', this.keyHandler);
|
}
|
maybeAddEmptyState() {
|
let empty = this.itemsList.querySelector('.empty');
|
if(empty) {
|
empty.remove();
|
}
|
|
if (this.cartItems.size === 0) {
|
this.checkoutPanel.disabled = true;
|
this.checkoutPanel.title = 'Add some things to your cart first!';
|
let empty = window.getTemplate('emptyCart');
|
this.itemsList.append(empty);
|
this.table.closest('table').hidden = true;
|
this.total.hidden = true;
|
this.a11y.announce('Nothing in Cart');
|
} else {
|
this.checkoutPanel.disabled = false;
|
this.table.closest('table').hidden = false;
|
this.total.hidden = false;
|
this.checkoutPanel.title = 'Checkout';
|
}
|
}
|
closeCart(message = 'Closed Cart') {
|
this.checkout.classList.remove('expanded');
|
this.toggle.title = 'Show Cart';
|
this.toggle.ariaExpanded = false;
|
|
this.toggle.querySelector('span').textContent = '';
|
this.a11y.announce(message);
|
document.removeEventListener('keydown', this.keyHandler);
|
}
|
handleEscape(e) {
|
if (e.key === 'Escape') {
|
this.closeCart('Closed Cart with escape key');
|
this.stepMultiplier = 1;
|
} else if (e.ctrlKey && e.shiftKey) {
|
this.stepMultiplier = Math.max(parseInt(this.stepMultiplier) * 100, 1000);
|
} else if (e.shiftKey) {
|
this.stepMultiplier = Math.max(parseInt(this.stepMultiplier) * 10, 1000);
|
}
|
}
|
|
handleAddToCart(item) {
|
let id = item.dataset.id;
|
this.createItemElement(item);
|
let price = parseFloat(item.dataset.price);
|
let quantity = parseInt(item.querySelector('.quantity-input')?.value)??1;
|
let total = parseFloat(price * quantity);
|
this.cartItems.set(id, {
|
post_id: id,
|
name: item.dataset.name,
|
price: price,
|
quantity: quantity,
|
total: total,
|
square_catalog_id: item.dataset.squareCatalogId
|
});
|
this.saveCart();
|
}
|
|
notifyRestoredCart() {
|
let restored = window.getTemplate('restoredCart');
|
this.checkout.querySelector('.tab-content[data-tab=cartItems]').insertBefore(restored, this.itemsList);
|
|
this.cartItems.forEach(item => {
|
console.log(item);
|
let element = window.getTemplate('cartItem');
|
let field = element.querySelector('.quantity');
|
let price = item.price;
|
let quantity = item.quantity;
|
[
|
field.dataset.id,
|
element.querySelector('label').textContent,
|
element.querySelector('.price').textContent,
|
field.dataset.price,
|
field.dataset.squareCatalogId,
|
element.querySelector('[name="quantity"]').value,
|
element.querySelector('.total').textContent
|
] = [
|
item['post_id'],
|
item.name,
|
window.formatPrice(price),
|
price,
|
item['square_catalog_id'],
|
quantity,
|
window.formatPrice(quantity * price)
|
];
|
this.table.append(element);
|
});
|
this.updateTotal();
|
}
|
|
handleRemoveFromCart(item) {
|
if (confirm('This will remove this item from the cart. Continue?')) {
|
if (!item.querySelector('[data-id]')) {
|
//it's a remove button
|
item = item.closest('.item')?.querySelector('.quantity.field');
|
}
|
let id = item.dataset.id;
|
this.cartItems.delete(id);
|
this.table.querySelector(`[data-id="${id}"]`)?.closest('tr').remove();
|
//Reset the field value to 0 too
|
let input = document.querySelector(`[data-id="${id}"] input`);
|
if (input){
|
input.value = 0;
|
}
|
this.maybeAddEmptyState();
|
this.saveCart();
|
}
|
}
|
|
clearCart() {
|
this.cartItems.clear();
|
window.removeChildren(this.table);
|
this.saveCart();
|
}
|
|
saveCart() {
|
this.updateTotal();
|
this.cache.set('cart', this.cartItems);
|
}
|
|
updateTotal() {
|
let total = 0;
|
this.cartItems.forEach(item => {
|
console.log(item);
|
total += item.total;
|
});
|
let tax = total * .05;
|
total = window.formatPrice(total + tax);
|
tax = window.formatPrice(tax);
|
window.eraseText(this.totalTax);
|
window.eraseText(this.grandTotal);
|
window.typeText(this.totalTax, tax);
|
window.typeText(this.grandTotal, total);
|
this.totalTax.classList.remove('typeText');
|
}
|
|
createItemElement(item) {
|
let element = this.itemsList.querySelector(`[data-id="${item.dataset.id}"]`);
|
let add = false;
|
let price = item.dataset.price;
|
let quantity = item.querySelector('[name="quantity"]')?.value??1;
|
if (!element) {
|
add = true;
|
element = window.getTemplate('cartItem');
|
let field = element.querySelector('.quantity');
|
[
|
field.dataset.id,
|
element.querySelector('label').textContent,
|
element.querySelector('.price').textContent,
|
field.dataset.price,
|
field.dataset.squareCatalogId
|
] = [
|
item.dataset.id,
|
item.dataset.name,
|
window.formatPrice(price),
|
price,
|
item.dataset.squareCatalogId
|
];
|
}else {
|
element = element.closest('tr');
|
}
|
|
[
|
element.querySelector('[name="quantity"]').value,
|
element.querySelector('.total').textContent
|
] = [
|
quantity,
|
window.formatPrice(quantity * price)
|
];
|
if (add) {
|
element.classList.add('adding');
|
this.table.append(element);
|
setTimeout(()=> {
|
element.classList.remove('adding');
|
}, 500);
|
}
|
|
}
|
|
async init() {
|
if (!window.Square) {
|
console.error('Square Web Payments SDK not loaded');
|
return;
|
}
|
|
try {
|
this.payments = window.Square.payments(
|
this.config.application_id,
|
this.config.location_id
|
);
|
|
await this.initializePaymentMethods();
|
|
this.isInitialized = true;
|
|
// Trigger ready event
|
document.dispatchEvent(new CustomEvent('squareCheckoutReady', {
|
detail: { checkout: this }
|
}));
|
|
} catch (error) {
|
console.error('Failed to initialize Square payments:', error);
|
this.handleError(error);
|
}
|
}
|
|
initElements() {
|
this.toggle = document.querySelector('.toggle-cart');
|
|
if (squareConfig.isOpen !== '1') {
|
this.toggle.disabled = true;
|
this.toggle.title = 'Currently closed for online ordering';
|
}
|
this.checkoutPanel = this.checkout.querySelector('button[data-tab="checkout"]');
|
this.itemsList = this.checkout.querySelector('.cart-items');
|
this.table = this.checkout.querySelector('.cart-items tbody');
|
this.total = this.checkout.querySelector('.cart-total');
|
this.totalTax = this.total.querySelector('.tax span');
|
this.grandTotal = this.total.querySelector('.total span');
|
this.checkoutForm = this.checkout.querySelector('form');
|
|
this.tabs = new window.jvbTabs(this.checkoutForm, {
|
updateURL: false
|
});
|
|
console.log('Initialized Checkout');
|
}
|
|
bindEvents() {
|
this.checkoutForm.addEventListener('submit', (e) => this.handleFormSubmit(e));
|
|
document.addEventListener('click', this.clickHandler);
|
document.addEventListener('change', this.changeHandler);
|
}
|
|
async initializePaymentMethods() {
|
const cardContainer = document.getElementById('square-card-container');
|
if (!cardContainer) return;
|
|
try {
|
this.card = await this.payments.card({
|
style: this.getCardStyle()
|
});
|
await this.card.attach('#square-card-container');
|
} catch (error) {
|
console.error('Failed to initialize card:', error);
|
throw error;
|
}
|
}
|
|
getCardStyle() {
|
return {
|
input: {
|
fontSize: '16px',
|
fontFamily: 'inherit',
|
color: '#333',
|
backgroundColor: '#fff'
|
},
|
'.input-container': {
|
borderColor: '#ccc',
|
borderRadius: '4px'
|
},
|
'.input-container.is-focus': {
|
borderColor: '#007cba'
|
},
|
'.input-container.is-error': {
|
borderColor: '#d63638'
|
}
|
};
|
}
|
|
async handleFormSubmit(event) {
|
if (squareConfig.isOpen !== '1') {
|
return;
|
}
|
event.preventDefault();
|
|
if (!this.isInitialized) {
|
this.handleError('Checkout not initialized');
|
return;
|
}
|
|
const form = event.target;
|
const orderData = this.extractOrderData(form);
|
|
try {
|
window.jvbLoading.showLoading('Processing payment...');
|
const result = await this.processPayment(orderData);
|
this.handleSuccess(result, form);
|
} catch (error) {
|
this.handleError(error);
|
} finally {
|
window.jvbLoading.hideLoading();
|
}
|
}
|
|
extractOrderData(form) {
|
// Convert cart items Map to array with proper structure
|
const items = Array.from(this.cartItems.values()).map(item => ({
|
post_id: item.post_id,
|
quantity: item.quantity,
|
price: item.price,
|
name: item.name
|
}));
|
|
const total = items.reduce((sum, item) =>
|
sum + (item.price * item.quantity), 0
|
);
|
|
return {
|
total: total * 100, // Square expects amount in cents
|
items: items,
|
customer: {
|
email: form.querySelector('[name="email"]')?.value || '',
|
name: form.querySelector('[name="name"]')?.value || '',
|
phone: form.querySelector('[name="phone"]')?.value || ''
|
},
|
note: form.querySelector('[name="special_instructions"]')?.value || '',
|
pickup_time: form.querySelector('[name="pickup_time"]')?.value || ''
|
};
|
}
|
|
async processPayment(orderData) {
|
try {
|
const result = await this.card.tokenize();
|
|
if (result.status === 'OK') {
|
return await this.submitToServer(result.token, orderData);
|
} else {
|
throw new Error('Card tokenization failed: ' + (result.errors?.join(', ') || 'Unknown error'));
|
}
|
} catch (error) {
|
console.error('Payment processing failed:', error);
|
throw error;
|
}
|
}
|
|
async submitToServer(token, orderData) {
|
if (squareConfig.isOpen !== '1') {
|
return;
|
}
|
|
// Square Web Payments SDK handles EVERYTHING
|
// We just need to track the order for status updates
|
const response = await fetch(this.config.api_url + 'save-order', {
|
method: 'POST',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': this.config.nonce
|
},
|
body: JSON.stringify({
|
order_id: token.orderId, // From Square SDK response
|
payment_id: token.paymentId, // From Square SDK response
|
customer: orderData.customer,
|
items: orderData.items,
|
action: 'jvb_integration_action',
|
service: 'square',
|
integration_action: 'save_order'
|
})
|
});
|
|
const result = await response.json();
|
|
if (!response.ok) {
|
throw new Error(result.message || 'Failed to save order');
|
}
|
|
return result;
|
}
|
|
trackOrder(orderNum) {
|
this.orderId = orderNum;
|
this.scheduleOrderCheck();
|
this.checkout.querySelector('button[data-tab=order]').hidden = false;
|
}
|
|
scheduleOrderCheck(){
|
window.debouncer.schedule(
|
'order',
|
() => { this.checkOrderStatus()},
|
30000 //30seconds
|
)
|
}
|
|
async checkOrderStatus() {
|
const response = await fetch(`/wp-json/jvb/v1/square/order-status/${this.orderId}`);
|
const data = await response.json();
|
if (data.status !== 'ready') {
|
this.scheduleOrderCheck();
|
}
|
this.updateOrderStatus(data);
|
}
|
|
updateOrderStatus(data) {
|
// Update status timeline
|
this.checkout.querySelectorAll('.status-item').forEach(item => {
|
if(item.dataset.status === data.status) {
|
item.classList.add('active');
|
}
|
});
|
|
// Update ETA
|
this.checkout.querySelector('#eta').textContent = data.eta || 'In progress';
|
}
|
/**************************************************************
|
* Customer Data
|
**************************************************************/
|
async loadCustomerProfile(email) {
|
const response = await fetch('/wp-json/jvb/v1/square/customer', {
|
method: 'POST',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': this.config.nonce
|
},
|
body: JSON.stringify({ email })
|
});
|
|
const profile = await response.json();
|
|
if (profile) {
|
this.displaySavedCards(profile.cards);
|
this.fillCustomerInfo(profile.customer);
|
}
|
}
|
|
displaySavedCards(cards) {
|
const container = document.getElementById('saved-cards');
|
if (!cards.length) return;
|
|
container.innerHTML = `
|
<h3>Saved Payment Methods</h3>
|
${cards.map(card => `
|
<label>
|
<input type="radio" name="payment_method" value="${card.id}">
|
•••• ${card.last_4} (${card.card_brand})
|
</label>
|
`).join('')}
|
<label>
|
<input type="radio" name="payment_method" value="new" checked>
|
Use new card
|
</label>
|
`;
|
}
|
|
handleSuccess(result, form) {
|
// Trigger success event
|
document.dispatchEvent(new CustomEvent('squareCheckoutSuccess', {
|
detail: { result, form }
|
}));
|
|
// Default redirect behavior
|
const successUrl = form.dataset.successUrl ||
|
`/order-confirmation/?order=${result.wp_order_id}`;
|
window.location.href = successUrl;
|
}
|
|
handleError(error) {
|
console.error('Square checkout error:', error);
|
|
// Trigger error event
|
document.dispatchEvent(new CustomEvent('squareCheckoutError', {
|
detail: { error }
|
}));
|
|
// Default error display
|
window.jvbNotifications?.show?.(error.message || 'Payment failed', 'error');
|
}
|
}
|
|
document.addEventListener('DOMContentLoaded', () =>{
|
window.squareCheckout = new SquareCheckout();
|
});
|
|
|
// Usage example
|
// document.addEventListener('DOMContentLoaded', async function() {
|
// // Get config from WordPress
|
// const configResponse = await fetch('/wp-json/jvb/v1/square/payment-form');
|
// const config = await configResponse.json();
|
//
|
// const checkout = new SquareCheckout(config);
|
//
|
// // Handle form submission
|
// document.getElementById('checkout-form').addEventListener('submit', async function(e) {
|
// e.preventDefault();
|
//
|
// const orderData = {
|
// total: parseFloat(this.dataset.total),
|
// items: JSON.parse(this.dataset.items),
|
// customer: {
|
// email: this.querySelector('[name="email"]').value,
|
// name: this.querySelector('[name="name"]').value
|
// }
|
// };
|
//
|
// try {
|
// const result = await checkout.processPayment(orderData);
|
// // Handle success - redirect or show confirmation
|
// window.location.href = '/order-confirmation/?order=' + result.wp_order_id;
|
// } catch (error) {
|
// // Handle error
|
// alert('Payment failed: ' + error.message);
|
// }
|
// });
|
// });
|