feat: migrate shipping to form-level and integrate flags.json as single source of truth
Shipping Migration: - Move shipping configuration from product-level to form-level - Add form shipping tab in form settings (no_shipping, flat_rate, free_shipping) - Update FlatRate to register at form level instead of product level - Update checkout logic to read from form settings - Support percentage-based flat rate calculation - Simplify shipping method IDs (flat_rate, free_shipping) Currency Flags Integration: - Add formipay_get_all_currency_flags() to read from admin/assets/json/flags.json - Remove hardcoded CURRENCY_FLAGS emoji map from VariationField.js - Create CurrencyFlag component to render base64 flag images - Localize currency_flags to window.formipayProductDetails - Update shipping info display in admin order details Benefits: - Form-level shipping prevents multiplying shipping costs per product - Single source of truth for currency flags (flags.json) - Better support for future cart system - Consistent with e-commerce standards
This commit is contained in:
346
public/assets/js/checkout-shipping.js
Normal file
346
public/assets/js/checkout-shipping.js
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Formipay Checkout Shipping Integration
|
||||
* Handles country selection, shipping method display, and cost calculation
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
const FormipayCheckoutShipping = {
|
||||
|
||||
selectedCountry: '',
|
||||
selectedMethod: '',
|
||||
availableMethods: [],
|
||||
formId: null,
|
||||
|
||||
init() {
|
||||
this.formId = $('form[data-form-id]').data('form-id');
|
||||
|
||||
if (!this.formId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bindEvents();
|
||||
this.initializeCountrySelector();
|
||||
},
|
||||
|
||||
bindEvents() {
|
||||
// Country selection change
|
||||
$(document).on('change', '.formipay-shipping-country', (e) => {
|
||||
this.onCountryChange($(e.currentTarget).val());
|
||||
});
|
||||
|
||||
// Shipping method selection change
|
||||
$(document).on('change', '.formipay-shipping-method', (e) => {
|
||||
this.onShippingMethodChange($(e.currentTarget).val());
|
||||
});
|
||||
},
|
||||
|
||||
initializeCountrySelector() {
|
||||
// Check if this form has shipping enabled
|
||||
if (this.isShippingEnabled()) {
|
||||
// Add country selector before payment options
|
||||
this.insertCountrySelector();
|
||||
}
|
||||
},
|
||||
|
||||
isShippingEnabled() {
|
||||
// Check if shipping is enabled for this form
|
||||
// With form-level shipping, we check shipping_enabled setting
|
||||
return window.formipayFormData?.shipping_enabled &&
|
||||
window.formipayFormData.shipping_enabled !== 'no_shipping';
|
||||
},
|
||||
|
||||
insertCountrySelector() {
|
||||
const orderReviewTable = $('#formipay-review-order');
|
||||
|
||||
if (orderReviewTable.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create country selector row before subtotal
|
||||
const countryRow = `
|
||||
<tr class="formipay-shipping-row">
|
||||
<th>
|
||||
<label for="shipping-country-${this.formId}">${formipay_shipping.labels.country || 'Shipping Country'}</label>
|
||||
<select id="shipping-country-${this.formId}"
|
||||
class="formipay-shipping-country"
|
||||
name="shipping_country"
|
||||
required>
|
||||
<option value="">${formipay_shipping.labels.selectCountry || 'Select your country'}</option>
|
||||
</select>
|
||||
</th>
|
||||
<td>
|
||||
<span class="formipay-loading-spinner" style="display:none;">⏳</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
orderReviewTable.find('tbody').append(countryRow);
|
||||
|
||||
// Populate countries from settings
|
||||
this.loadCountries();
|
||||
},
|
||||
|
||||
loadCountries() {
|
||||
// Get supported countries from shipping settings
|
||||
$.ajax({
|
||||
url: formipay_admin.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'formipay_get_supported_countries',
|
||||
nonce: formipay_admin.nonce,
|
||||
form_id: this.formId
|
||||
},
|
||||
success: (response) => {
|
||||
if (response.success && response.data.countries) {
|
||||
this.populateCountries(response.data.countries);
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
// Fallback: show all countries
|
||||
this.populateCountries(this.getAllCountries());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
populateCountries(countries) {
|
||||
const select = $(`#shipping-country-${this.formId}`);
|
||||
select.find('option:not([value=""])').remove();
|
||||
|
||||
$.each(countries, (code, name) => {
|
||||
select.append(`<option value="${code}">${name}</option>`);
|
||||
});
|
||||
},
|
||||
|
||||
getAllCountries() {
|
||||
// Fallback country list
|
||||
return {
|
||||
'ID': 'Indonesia',
|
||||
'MY': 'Malaysia',
|
||||
'SG': 'Singapore',
|
||||
'TH': 'Thailand',
|
||||
'VN': 'Vietnam',
|
||||
'PH': 'Philippines',
|
||||
'TW': 'Taiwan',
|
||||
'HK': 'Hong Kong',
|
||||
'IN': 'India',
|
||||
'CN': 'China',
|
||||
'JP': 'Japan',
|
||||
'KR': 'South Korea',
|
||||
'AU': 'Australia',
|
||||
'NZ': 'New Zealand',
|
||||
'GB': 'United Kingdom',
|
||||
'US': 'United States',
|
||||
'CA': 'Canada',
|
||||
'FR': 'France',
|
||||
'DE': 'Germany',
|
||||
'IT': 'Italy',
|
||||
'ES': 'Spain',
|
||||
'NL': 'Netherlands',
|
||||
'BE': 'Belgium',
|
||||
'CH': 'Switzerland',
|
||||
'AT': 'Austria',
|
||||
'IE': 'Ireland',
|
||||
'DK': 'Denmark',
|
||||
'SE': 'Sweden',
|
||||
'NO': 'Norway',
|
||||
'FI': 'Finland',
|
||||
};
|
||||
},
|
||||
|
||||
onCountryChange(countryCode) {
|
||||
this.selectedCountry = countryCode;
|
||||
|
||||
if (!countryCode) {
|
||||
this.hideShippingMethods();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get available shipping methods for this country
|
||||
this.fetchShippingMethods(countryCode);
|
||||
},
|
||||
|
||||
fetchShippingMethods(countryCode) {
|
||||
const spinner = $('.formipay-loading-spinner');
|
||||
spinner.show();
|
||||
|
||||
$.ajax({
|
||||
url: formipay_admin.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'formipay_get_shipping_methods',
|
||||
nonce: formipay_public.nonce,
|
||||
form_id: this.formId,
|
||||
country: countryCode,
|
||||
currency: formipay.currency_code || 'IDR'
|
||||
},
|
||||
success: (response) => {
|
||||
spinner.hide();
|
||||
|
||||
if (response.success) {
|
||||
this.availableMethods = response.data.methods || [];
|
||||
this.displayShippingMethods(this.availableMethods, response.data.default_method);
|
||||
} else {
|
||||
this.showError(response.data.message || 'Unable to load shipping methods');
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
spinner.hide();
|
||||
this.showError('Unable to connect to shipping service');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
displayShippingMethods(methods, defaultMethod) {
|
||||
// Remove existing shipping method selector if any
|
||||
$('.formipay-shipping-method-row').remove();
|
||||
|
||||
if (methods.length === 0) {
|
||||
this.showError('Shipping is not available for this form');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get order total for percentage calculations
|
||||
const orderTotal = this.getOrderTotal();
|
||||
|
||||
let methodsHtml = '<div class="formipay-shipping-methods">';
|
||||
methodsHtml += `<input type="hidden" name="shipping_method" value="${defaultMethod}" class="formipay-shipping-method-input">`;
|
||||
|
||||
$.each(methods, (index, method) => {
|
||||
const methodId = method.id;
|
||||
const isFree = method.cost === 0;
|
||||
const isPercentage = method.type === 'percentage';
|
||||
|
||||
// Calculate actual cost
|
||||
let actualCost = method.cost;
|
||||
let costDisplay = '';
|
||||
|
||||
if (isFree) {
|
||||
costDisplay = 'FREE';
|
||||
} else if (isPercentage) {
|
||||
actualCost = (orderTotal * method.cost) / 100;
|
||||
costDisplay = `${method.cost}% (${this.formatCost(actualCost, method.currency)})`;
|
||||
} else {
|
||||
costDisplay = this.formatCost(method.cost, method.currency);
|
||||
}
|
||||
|
||||
methodsHtml += `
|
||||
<div class="formipay-shipping-option" data-method="${methodId}">
|
||||
<label class="formipay-shipping-label">
|
||||
<input type="radio"
|
||||
name="shipping_method_display"
|
||||
value="${methodId}"
|
||||
${methodId === defaultMethod ? 'checked' : ''}
|
||||
class="formipay-shipping-method"
|
||||
data-cost="${actualCost}"
|
||||
data-type="${method.type || 'fixed'}"
|
||||
data-base-cost="${method.cost}">
|
||||
<span class="shipping-method-name">${method.name}</span>
|
||||
${isFree ? '<span class="badge badge-free">FREE</span>' : ''}
|
||||
<span class="shipping-method-cost">${costDisplay}</span>
|
||||
${method.description ? `<span class="shipping-method-desc">${method.description}</span>` : ''}
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
methodsHtml += '</div>';
|
||||
|
||||
// Insert after country selector
|
||||
const countryRow = $('.formipay-shipping-row');
|
||||
const shippingRow = `
|
||||
<tr class="formipay-shipping-method-row">
|
||||
<td colspan="2">
|
||||
${methodsHtml}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
countryRow.after(shippingRow);
|
||||
|
||||
// Bind shipping method change events
|
||||
$('.formipay-shipping-method').on('change', (e) => {
|
||||
const target = $(e.currentTarget);
|
||||
const cost = parseFloat(target.data('cost'));
|
||||
const type = target.data('type');
|
||||
const baseCost = parseFloat(target.data('base-cost'));
|
||||
const method = target.val();
|
||||
|
||||
// For percentage, recalculate in case order total changed
|
||||
let finalCost = cost;
|
||||
if (type === 'percentage') {
|
||||
finalCost = (this.getOrderTotal() * baseCost) / 100;
|
||||
}
|
||||
|
||||
// Update hidden input
|
||||
$('.formipay-shipping-method-input').val(method);
|
||||
|
||||
// Update order total
|
||||
this.updateOrderTotal(finalCost);
|
||||
});
|
||||
|
||||
// Trigger change on default method to set initial cost
|
||||
$(`input[name="shipping_method_display"][value="${defaultMethod}"]`).trigger('change');
|
||||
},
|
||||
|
||||
hideShippingMethods() {
|
||||
$('.formipay-shipping-method-row').remove();
|
||||
},
|
||||
|
||||
getOrderTotal() {
|
||||
// Get current order total (excluding shipping)
|
||||
const subtotalRow = $('.formipay-total-row td').text();
|
||||
const subtotal = this.parseCurrency(subtotalRow);
|
||||
return subtotal;
|
||||
},
|
||||
|
||||
updateOrderTotal(shippingCost) {
|
||||
const currentTotal = this.getOrderTotal();
|
||||
const newTotal = currentTotal + shippingCost;
|
||||
|
||||
// Update the total display
|
||||
const totalRow = $('.formipay-grand-total-row td');
|
||||
totalRow.text(this.formatCost(newTotal));
|
||||
|
||||
// Update submit button
|
||||
const submitBtn = $('.formipay-submit-button');
|
||||
const currentText = submitBtn.attr('data-button-text');
|
||||
submitBtn.html(`${currentText} - ${this.formatCost(newTotal)}`);
|
||||
},
|
||||
|
||||
formatCost(cost, currency) {
|
||||
// Format cost using same format as product prices
|
||||
const formatted = cost.toFixed(formipay.decimal_digits || 2);
|
||||
return (currency || formipay.currency || '') + ' ' + formatted;
|
||||
},
|
||||
|
||||
parseCurrency(text) {
|
||||
// Parse currency string to get numeric value
|
||||
// Removes currency symbols and formats
|
||||
const numeric = text.replace(/[^\d.-]/g, '');
|
||||
return parseFloat(numeric) || 0;
|
||||
},
|
||||
|
||||
showError(message) {
|
||||
const errorHtml = `
|
||||
<div class="formipay-shipping-error notice notice-error">
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
`;
|
||||
$('.formipay-shipping-method-row').html(`<td colspan="2">${errorHtml}</td>`);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Initialize when document is ready
|
||||
$(document).ready(() => {
|
||||
if (typeof formipay_shipping !== 'undefined' && typeof formipay_shipping.labels !== 'undefined') {
|
||||
FormipayCheckoutShipping.init();
|
||||
}
|
||||
});
|
||||
|
||||
// Expose for global access
|
||||
window.FormipayCheckoutShipping = FormipayCheckoutShipping;
|
||||
|
||||
})(jQuery);
|
||||
Reference in New Issue
Block a user