/**
|
* CheckoutSquare — Square Web Payments SDK checkout
|
*
|
* Extends CartCheckout with:
|
* - Square Web Payments SDK initialization
|
* - Card tokenization (new card or saved card)
|
* - Square-specific API submission
|
* - Saved cards via Square Customers API
|
*
|
* All cart management, totals, UI, order tracking, etc. are
|
* inherited from CartCheckout (cart-checkout.min.js).
|
*
|
* HTML structure: Checkout.php (JVBase\ui\Checkout)
|
* - Mounts card form into #payment-container
|
* - Uses data-catalog-id (mapped to catalog_object_id for Square API)
|
*/
|
class CheckoutSquare extends window.jvbCheckout {
|
constructor(config = {}) {
|
super({
|
...window.squareConfig,
|
...config,
|
});
|
|
// Square-specific
|
this.payments = null;
|
this.card = null;
|
}
|
|
/*****************************************************************
|
* INIT — Square Web Payments SDK
|
*****************************************************************/
|
|
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;
|
|
document.dispatchEvent(new CustomEvent('checkoutReady', {
|
detail: { checkout: this, provider: 'square' }
|
}));
|
|
} catch (error) {
|
console.error('Failed to initialize Square payments:', error);
|
this.handleError(error);
|
}
|
}
|
|
async initializePaymentMethods() {
|
const cardContainer = document.getElementById('payment-container');
|
if (!cardContainer) return;
|
|
try {
|
this.card = await this.payments.card({
|
style: this.getCardStyle()
|
});
|
await this.card.attach('#payment-container');
|
|
this.card.addEventListener('cardBrandChanged', (event) => {
|
console.log('Card brand:', event.detail.cardBrand);
|
});
|
} 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: '#006AFF',
|
borderWidth: '2px',
|
outline: '2px solid #006AFF',
|
outlineOffset: '2px'
|
},
|
'.input-container.is-error': {
|
borderColor: '#d63638'
|
}
|
};
|
}
|
|
/*****************************************************************
|
* PAYMENT — Square tokenization
|
*****************************************************************/
|
|
async processPayment(orderData) {
|
try {
|
let sourceToken = null;
|
|
if (this.selectedCardId) {
|
// Use saved card
|
sourceToken = this.selectedCardId;
|
} else {
|
// Tokenize new card
|
const tokenResult = await this.card.tokenize({
|
verificationDetails: {
|
amount: String(orderData.total),
|
currencyCode: this.config.currency || 'CAD',
|
intent: 'CHARGE',
|
customerInitiated: true,
|
billingContact: {
|
givenName: orderData.customer.name.split(' ')[0],
|
familyName: orderData.customer.name.split(' ').slice(1).join(' '),
|
email: orderData.customer.email,
|
phone: orderData.customer.phone,
|
}
|
}
|
});
|
|
if (tokenResult.status !== 'OK') {
|
const errors = tokenResult.errors?.map(e => e.message).join(', ') || 'Unknown error';
|
throw new Error(`Card tokenization failed: ${errors}`);
|
}
|
|
sourceToken = tokenResult.token;
|
|
if (tokenResult.details?.userChallenged) {
|
console.log('3D Secure verification completed');
|
}
|
}
|
|
return await this.submitToServer(sourceToken, orderData, !!this.selectedCardId);
|
|
} catch (error) {
|
console.error('Payment processing failed:', error);
|
throw error;
|
}
|
}
|
|
/*****************************************************************
|
* SERVER — Square REST endpoint
|
*****************************************************************/
|
|
async submitToServer(sourceToken, orderData, isSavedCard = false) {
|
if (!this.isOpen) {
|
throw new Error('Store is currently closed');
|
}
|
|
const response = await fetch(this.config.api_url + 'process-payment', {
|
method: 'POST',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': this.config.nonce,
|
},
|
body: JSON.stringify({
|
source_id: sourceToken,
|
is_saved_card: isSavedCard,
|
cart_id: this.getCartId(),
|
amount: orderData.total,
|
items: orderData.items,
|
customer: {
|
email: this.isLoggedIn ? this.userEmail : orderData.customer.email,
|
name: orderData.customer.name,
|
phone: orderData.customer.phone,
|
},
|
note: orderData.note,
|
pickup_time: orderData.pickup_time,
|
}),
|
});
|
|
const result = await response.json();
|
|
if (!response.ok) {
|
throw new Error(result.message || 'Payment processing failed');
|
}
|
|
this.clearCart();
|
return result;
|
}
|
|
/*****************************************************************
|
* ORDER DATA — Square-specific field mapping
|
*
|
* Base class builds the generic structure. We override to map
|
* catalog_id → catalog_object_id for the Square Orders API.
|
*****************************************************************/
|
|
extractOrderData(form) {
|
const base = super.extractOrderData(form);
|
|
// Remap for Square's Orders API
|
base.items = base.items.map(item => ({
|
catalog_object_id: item.catalog_id,
|
quantity: item.quantity,
|
price: item.price,
|
note: item.note,
|
}));
|
|
return base;
|
}
|
|
/*****************************************************************
|
* SAVED CARDS — Square Customers API
|
*****************************************************************/
|
|
async loadSavedCards() {
|
try {
|
const response = await fetch(this.config.api_url + 'saved-cards', {
|
method: 'GET',
|
headers: { 'X-WP-Nonce': this.config.nonce },
|
});
|
|
const result = await response.json();
|
|
if (result.success && result.cards) {
|
this.savedCards = result.cards;
|
this.renderSavedCards();
|
}
|
} catch (error) {
|
console.error('Failed to load saved cards:', error);
|
}
|
}
|
}
|
|
/*****************************************************************
|
* BOOTSTRAP — only init if Square is the active provider
|
*****************************************************************/
|
|
document.addEventListener('DOMContentLoaded', () => {
|
const form = document.querySelector('#checkout[data-provider="square"]');
|
if (form) {
|
window.squareCheckout = new CheckoutSquare();
|
}
|
});
|