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:
dwindown
2026-04-23 08:12:40 +07:00
parent 0094a3571c
commit 008188b790
13 changed files with 2819 additions and 797 deletions

View File

@@ -64,6 +64,28 @@ jQuery(function($){
$('#order-total').html(res.total_formatted);
$('#order_status').val(res.status);
// Populate shipping info if available
var shippingInfo = [];
if(res.form_data){
$.each(res.form_data, function(key, data){
if(data.name === 'shipping_country' || data.name === 'shipping_method'){
shippingInfo.push(data);
}
});
}
if(shippingInfo.length > 0){
var source = $("#shipping-info-template").html();
var template = Handlebars.compile(source);
var context = {
datas: shippingInfo
};
var html = template(context);
$("#shipping-info-list").html(html);
} else {
$("#shipping-info-list").addClass('d-none');
$('#no-shipping-info').removeClass('d-none');
}
var source = $("#form-data-item-template").html();
var template = Handlebars.compile(source);
var context = {

View File

@@ -122,15 +122,21 @@ function get_global_currency_array() {
if(false === $ifSingleCurrency){
// $currency_sort = [];
$default_sort_key = null;
// Extract currency code from default_currency for comparison (handles case where default has symbol but multicurrencies don't)
$default_currency_code = explode(':::', $default_currency)[0];
foreach($global_currencies as $key => $currency){
$currency_value = $currency['currency'];
if($currency_value === $default_currency){
// Compare by currency code only (before first :::)
$currency_code = explode(':::', $currency_value)[0];
if($currency_code === $default_currency_code){
$default_sort_key = $key;
}
}
$currency_sort = [$default_sort_key => $global_currencies[$default_sort_key]];
unset($global_currencies[$default_sort_key]);
$global_currencies = $currency_sort + $global_currencies;
// Convert associative array to indexed array for JavaScript
$global_currencies = array_values($global_currencies);
}else{
if(false === boolval($multicurrency)){
$global_currencies = [
@@ -176,6 +182,31 @@ function formipay_get_flag_by_currency($currency) {
}
/**
* Get all currency flags from flags.json
* Returns an array mapping currency codes to base64 flag images
* This is the single source of truth for currency flags - never duplicate this data
*
* @return array Array of currency code => flag image mapping
*/
function formipay_get_all_currency_flags() {
static $currency_flags = null;
if ($currency_flags !== null) {
return $currency_flags;
}
$json = file_get_contents(FORMIPAY_PATH . 'admin/assets/json/flags.json');
$flags = json_decode($json, true);
$currency_flags = [];
foreach ($flags as $item) {
$currency_flags[$item['code']] = $item['flag'];
}
return $currency_flags;
}
function formipay_price_format($num = 0, $post_id = 0){
$decimal_digits = 2;
@@ -362,6 +393,14 @@ function formipay_get_order($order_id) {
$label = esc_html__( 'Payment Gateway', 'formipay' );
break;
case 'shipping_country':
$label = esc_html__( 'Shipping Country', 'formipay' );
break;
case 'shipping_method':
$label = esc_html__( 'Shipping Method', 'formipay' );
break;
default:
if(!empty($all_fields[$name.'_config'])){
$label = $all_fields[$name.'_config']['label'];

View File

@@ -55,6 +55,26 @@
</div>
</div>
</div>
<div class="order-detail-card shipping-info-card">
<div class="card-title mt-3 mb-0"><?php echo esc_html__( 'Shipping Information', 'formipay' ); ?></div>
<div class="card mt-1 border-0 rounded-4 shadow-sm">
<div class="card-body p-0 placeholder-glow">
<ul class="list-group list-group-flush" id="shipping-info-list">
<li class="list-group-item">
<b><span class="placeholder col-3"></span></b>
<p class="mb-0"><span class="placeholder col-8"></span></p>
</li>
<li class="list-group-item">
<b><span class="placeholder col-3"></span></b>
<p class="mb-0"><span class="placeholder col-8"></span></p>
</li>
</ul>
<div class="d-none" id="no-shipping-info">
<p class="text-center text-muted my-3"><?php echo esc_html__( 'No shipping information available', 'formipay' ); ?></p>
</div>
</div>
</div>
</div>
<div class="order-detail-card form-data-card">
<div class="card-title mt-3 w-100 mb-0 d-flex justify-content-between align-items-center">
<?php echo esc_html__( 'Form Data', 'formipay' ); ?>
@@ -274,3 +294,11 @@
<p class="mb-0">******</p>
</li>
</script>
<script id="shipping-info-template" type="text/x-handlebars-template">
{{#each datas as |data|}}
<li class="list-group-item px-0">
<b class="field-name">{{data.label}}</b>
<p class="field-value mt-1 mb-0">{{data.value}}</p>
</li>
{{/each}}
</script>

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'wp-components', 'wp-element', 'wp-i18n', 'wp-icons/build/arrow-left', 'wp-icons/build/bell', 'wp-icons/build/message', 'wp-icons/build/trash', 'wp-primitives'), 'version' => '66ba2a0809137b4a1bf3');
<?php return array('dependencies' => array('react', 'react-dom', 'wp-components', 'wp-element', 'wp-i18n', 'wp-icons/build/arrow-left', 'wp-icons/build/bell', 'wp-icons/build/message', 'wp-icons/build/trash'), 'version' => '79dab88e37717bf64790');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -33,6 +33,7 @@ class Product {
add_action( 'wp_ajax_formipay_product_get_currencies', [$this, 'formipay_product_get_currencies'] );
add_action( 'wp_ajax_get_product_variables', [$this, 'get_product_variables'] );
add_action( 'wp_ajax_get_product_attributes', [$this, 'get_product_attributes'] );
add_action('save_post', [$this, 'save_product'], 10, 2);
@@ -85,14 +86,21 @@ class Product {
public function add_submenu() {
add_action( 'add_meta_boxes', [$this, 'add_react_metabox'] );
add_action( 'admin_footer-post.php', [$this, 'render_react_metabox_template'] );
add_action( 'admin_footer-post-new.php', [$this, 'render_react_metabox_template'] );
add_action( 'save_post', [$this, 'save_product_metabox_fields'], 10, 2 );
add_submenu_page(
'formipay',
__('Products', 'formipay'),
__('Products', 'formipay'),
'manage_options',
'formipay-products',
[$this, 'formipay_products'],
[$this, 'formipay_product'],
2
);
add_submenu_page(
'formipay',
__('Categories', 'formipay'),
@@ -105,6 +113,10 @@ class Product {
}
public function formipay_product() {
\Formipay\Admin\ReactAdmin::render_mount_point('products');
}
public function enqueue_admin() {
// Assets now handled by ReactAdmin class
return;
@@ -227,9 +239,71 @@ class Product {
}
public function formipay_products_react() {
ReactAdmin::render_mount_point('products');
}
public function add_react_metabox() {
add_meta_box(
'formipay_product_settings',
__('Settings', 'formipay'),
[$this, 'render_react_metabox'],
'formipay-product',
'normal',
'high'
);
}
public function render_react_metabox($post) {
echo '<div data-formipay-field-renderer="product" data-post-id="' . esc_attr($post->ID) . '"></div>';
}
public function render_react_metabox_template() {
global $post;
if (!$post || $post->post_type !== 'formipay-product') {
return;
}
$config = \Formipay\Admin\FieldConfigBridge::get_config_for_post($post->ID, $post->post_type);
// Get multi-currency settings from formipay_settings option
$settings = get_option('formipay_settings', []);
$is_multicurrency = !empty($settings['enable_multicurrency']);
$multicurrencies = $settings['multicurrencies'] ?? [];
// Build global_selected_currencies from multicurrencies array
$global_selected = [];
foreach ($multicurrencies as $currency) {
if (isset($currency['currency'])) {
$code = explode(':::', $currency['currency'])[0];
$global_selected[$code] = true;
}
}
// Use the same helper functions for consistency
$global_currencies = get_global_currency_array();
$default_currency = formipay_default_currency();
$product_details = [
'multicurrency' => $is_multicurrency,
'default_currency' => $default_currency,
'global_currencies' => $global_currencies,
'global_selected_currencies' => $global_selected,
'currency_flags' => formipay_get_all_currency_flags(),
];
?>
<script type="text/javascript">
window.formipayFieldConfig = <?php echo wp_json_encode($config); ?>;
window.formipayProductDetails = <?php echo wp_json_encode($product_details); ?>;
</script>
<?php
}
public function formipay_products() {
// React admin
\Formipay\Admin\ReactAdmin::render_mount_point('products');
ReactAdmin::render_mount_point('products');
}
public function cpt_post_fields_box($boxes) {
@@ -248,7 +322,106 @@ class Product {
$fields = apply_filters( 'formipay/product-config', $fields );
return $fields;
}
public function save_product_metabox_fields($post_id, $post) {
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return $post_id;
}
if (!current_user_can('manage_options')) {
return $post_id;
}
if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'update-post_' . $post_id)) {
return $post_id;
}
if ($post->post_type !== 'formipay-product') {
return $post_id;
}
$meta_fields = [
'product_type',
'setting_product_price_regular',
'setting_product_price_sale',
'free_shipping',
// Shipping fields
'shipping_method',
'flat_rate_type',
'flat_rate_amount',
'flat_rate_label',
'free_shipping_label',
'free_shipping_add_to_order_review',
// Shipping dimensions (for carrier API calculation)
'product_weight',
'product_length',
'product_width',
'product_height',
// Access and status
'product_accesses',
'product_access_to_email',
'active',
];
$global_currencies = get_global_currency_array();
foreach ($meta_fields as $field) {
if (isset($_POST[$field])) {
$value = wp_unslash($_POST[$field]);
update_post_meta($post_id, $field, $value);
}
}
foreach ($global_currencies as $currency) {
$symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
if (isset($_POST['setting_product_price_regular_' . $symbol])) {
update_post_meta($post_id, 'setting_product_price_regular_' . $symbol, wp_unslash($_POST['setting_product_price_regular_' . $symbol]));
}
if (isset($_POST['max_amount_' . $symbol])) {
update_post_meta($post_id, 'max_amount_' . $symbol, wp_unslash($_POST['max_amount_' . $symbol]));
}
// Save flat rate amounts per currency
if (isset($_POST['flat_rate_amount_' . $symbol])) {
update_post_meta($post_id, 'flat_rate_amount_' . $symbol, wp_unslash($_POST['flat_rate_amount_' . $symbol]));
}
}
// Save product variations (JSON from VariationField)
if (isset($_POST['product_variations'])) {
$variations_json = wp_unslash($_POST['product_variations']);
$variations_data = json_decode($variations_json, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($variations_data)) {
update_post_meta($post_id, 'product_variations', $variations_json);
// Also save the legacy format for backward compatibility
if (isset($variations_data['variations']) && is_array($variations_data['variations'])) {
$legacy_variations = $variations_data['variations'];
update_post_meta($post_id, 'product_variables', wp_json_encode($legacy_variations));
}
// Save attributes separately for legacy compatibility
if (isset($variations_data['attributes']) && is_array($variations_data['attributes'])) {
$legacy_attributes = [];
foreach ($variations_data['attributes'] as $attr) {
$legacy_attributes[] = [
'attribute_name' => $attr['attribute_name'] ?? '',
'attribute_type' => $attr['attribute_type'] ?? 'select',
'attribute_variations' => $attr['attribute_variations'] ?? [],
];
}
update_post_meta($post_id, 'product_variation_attributes', wp_json_encode($legacy_attributes));
}
} else {
delete_post_meta($post_id, 'product_variations');
}
}
return $post_id;
}
public function general_config($fields) {
@@ -434,7 +607,68 @@ class Product {
$product_currency_group = apply_filters( 'formipay/product-settings/tab:general/group:product-currency', $product_currency_group );
$general_all_fields = array_merge($product_details_group, $product_currency_group);
// Shipping Dimensions Group (for physical products only)
$shipping_dimensions_group = array(
'setting_product_shipping_dimensions' => array(
'type' => 'group_title',
'label' => __( 'Shipping Dimensions', 'formipay' ),
'description' => __( 'Weight and dimensions for carrier shipping calculation.', 'formipay' ),
'dependency' => array(
'key' => 'product_type',
'value' => 'physical'
),
'group' => 'started'
),
'product_weight' => array(
'type' => 'number',
'label' => __( 'Weight (kg)', 'formipay' ),
'step' => 0.01,
'min' => 0,
'placeholder' => '0.00',
'dependency' => array(
'key' => 'product_type',
'value' => 'physical'
)
),
'product_length' => array(
'type' => 'number',
'label' => __( 'Length (cm)', 'formipay' ),
'step' => 0.1,
'min' => 0,
'placeholder' => '0.0',
'dependency' => array(
'key' => 'product_type',
'value' => 'physical'
)
),
'product_width' => array(
'type' => 'number',
'label' => __( 'Width (cm)', 'formipay' ),
'step' => 0.1,
'min' => 0,
'placeholder' => '0.0',
'dependency' => array(
'key' => 'product_type',
'value' => 'physical'
)
),
'product_height' => array(
'type' => 'number',
'label' => __( 'Height (cm)', 'formipay' ),
'step' => 0.1,
'min' => 0,
'placeholder' => '0.0',
'dependency' => array(
'key' => 'product_type',
'value' => 'physical'
),
'group' => 'ended'
),
);
$shipping_dimensions_group = apply_filters( 'formipay/product-settings/tab:general/group:shipping-dimensions', $shipping_dimensions_group );
$general_all_fields = array_merge($product_details_group, $product_currency_group, $shipping_dimensions_group);
$general_all_fields = apply_filters( 'formipay/product-settings/tab:general', $general_all_fields );
@@ -448,77 +682,28 @@ class Product {
}
public function variations_config($fields) {
// Product Variations Attribute Group
$product_attributes_group = array(
// Product Variations - Unified React field with attributes and variations table
$product_variations_group = array(
'setting_product_variations' => array(
'type' => 'group_title',
'label' => __( 'Attributes', 'formipay' ),
'description' => __( 'First we need to build the attribute of this product. For example Color, Size, License Site Count.', 'formipay' ),
'label' => __( 'Product Variations', 'formipay' ),
'description' => __( 'Configure attributes and their combinations to generate product variations with multi-currency pricing.', 'formipay' ),
'group' => 'started'
),
'product_has_variation' => array(
'type' => 'checkbox',
'label' => __( 'Product has variations', 'formipay' ),
),
'product_variation_attributes' => array(
'type' => 'repeater',
'label' => __('Attributes', 'formipay'),
'description' => __( 'Your attributes will generate variation automatically.', 'formipay' ),
'fields' => [
'attribute_name' => [
'type' => 'text',
'label' => __( 'Attribute Name', 'formipay' ),
'description' => __( 'e.g. Color, Size, etc', 'formipay' ),
'is_group_title' => true
],
'attribute_variations' => [
'type' => 'repeater',
'label' => esc_html__( 'Variation', 'formipay' ),
'fields' => [
'variation_label' => [
'type' => 'text',
'label' => __( 'Title', 'formipay' ),
'description' => __( 'e.g. Red, XL, etc', 'formipay' ),
'required' => true,
'is_group_title' => true
],
'variation_value' => [
'type' => 'text',
'label' => __( 'Value', 'formipay' ),
'description' => __( 'e.g. red, xl, etc', 'formipay' ),
'required' => true
]
],
],
],
'dependency' => array(
'key' => 'product_has_variation',
'value' => 'not_empty'
),
'product_variations' => array(
'type' => 'variation',
'label' => __( 'Variations', 'formipay' ),
'description' => __( 'Add attributes (e.g., Color, Size) and their values. Variations will be generated automatically from all combinations.', 'formipay' ),
'name' => 'product_variations',
),
);
$product_attributes_group = apply_filters( 'formipay/product-settings/tab:general/group:product-attributes', $product_attributes_group );
$product_variations_group = apply_filters( 'formipay/product-settings/tab:variations/group:product-variations', $product_variations_group );
$last_product_attributes_group = array_key_last($product_attributes_group);
$product_attributes_group[$last_product_attributes_group]['group'] = 'ended';
$last_product_variations_group = array_key_last($product_variations_group);
$product_variations_group[$last_product_variations_group]['group'] = 'ended';
// Product Variations Attribute Group
// Define your product variations field group somewhere in your plugin/theme
$product_variations_table_html = file_get_contents(FORMIPAY_PATH . 'admin/templates/product-variations.php');
$product_variations_group = [
'variation_table' => [
'type' => 'html',
'label' => __( 'Variations', 'formipay' ),
'html' => $product_variations_table_html
],
];
$variation_all_fields = array_merge($product_attributes_group, $product_variations_group);
$variation_all_fields = apply_filters( 'formipay/product-settings/tab:variations', $variation_all_fields );
$variation_all_fields = apply_filters( 'formipay/product-settings/tab:variations', $product_variations_group );
$fields['formipay_product_settings']['variation'] = array(
'name' => __('Variations', 'formipay'),
@@ -959,6 +1144,25 @@ class Product {
wp_send_json_error();
}
public function get_product_attributes() {
$post_id = intval($_POST['post_id'] ?? 0);
// Check permissions
if (!current_user_can('edit_post', $post_id)) {
wp_send_json_error(['message' => 'Unauthorized']);
}
// Get attributes from legacy meta key
$data = get_post_meta($post_id, 'product_variation_attributes', true);
$json = is_string($data) ? json_decode($data, true) : $data;
if (is_array($json)) {
wp_send_json_success($json);
}
wp_send_json_success([]);
}
public function save_product_depracated($post_id, $post) {
// Verify nonce and permissions here if you have a nonce field (recommended)

View File

@@ -653,6 +653,7 @@ class Render {
wp_enqueue_script( 'choices', FORMIPAY_URL . 'vendor/ChoicesJS/choices.min.js', [], FORMIPAY_VERSION, true );
wp_enqueue_script( 'formipay-popup', FORMIPAY_URL . 'public/assets/js/popup-action.js', ['jquery', 'choices'], FORMIPAY_VERSION, true);
wp_enqueue_script( 'formipay-form', FORMIPAY_URL . 'public/assets/js/form-action.js', ['jquery', 'choices'], FORMIPAY_VERSION, true);
wp_enqueue_script( 'formipay-checkout-shipping', FORMIPAY_URL . 'public/assets/js/checkout-shipping.js', ['jquery', 'formipay-form'], FORMIPAY_VERSION, true);
// Localize data for all forms
$form_data = [
'ajax_url' => admin_url('admin-ajax.php'),
@@ -662,6 +663,16 @@ class Render {
];
wp_localize_script('formipay-form', 'formipay_form', $form_data);
// Localize shipping labels for checkout
$shipping_data = [
'labels' => [
'country' => __('Shipping Country', 'formipay'),
'selectCountry' => __('Select your country', 'formipay'),
'shippingMethod' => __('Shipping Method', 'formipay'),
]
];
wp_localize_script('formipay-checkout-shipping', 'formipay_shipping', $shipping_data);
}
/**
@@ -766,6 +777,11 @@ class Render {
$allowed_currency_pack = $this->resolve_allowed_currencies($post_id);
$currency_code = $allowed_currency_pack['default_code'];
$currency_cfg = $this->resolve_currency_config($currency_code);
// Get form shipping settings
$form_settings = get_post_meta($post_id, 'formipay_form_settings', true);
$shipping_enabled = $form_settings['shipping_enabled'] ?? 'no_shipping';
$form_data[$post_id] = [
'form_id' => $post_id,
'currency' => formipay_post_currency($post_id),
@@ -796,6 +812,7 @@ class Render {
'static_products' => array_filter(array_map('absint', explode(',', (string) formipay_get_post_meta($post_id, 'static_products')))),
'static_items' => json_decode((string) formipay_get_post_meta($post_id, 'static_items'), true) ?: [],
'currency_code' => (function($c){ $p = explode(':::', (string)$c); return $p[0] ?? 'IDR'; })(formipay_post_currency($post_id)),
'shipping_enabled' => $shipping_enabled, // Form-level shipping setting
];
}

View File

@@ -15,8 +15,9 @@ class FlatRate extends Shipping {
parent::__construct();
add_filter( 'formipay/product-config/tab:shipping/method', [$this, 'add_shipping_method'], 15 );
add_filter( 'formipay/product-config/tab:shipping', [$this, 'add_shipping_settings'], 15 );
// Register flat rate as a form-level shipping method
add_filter( 'formipay/form-settings/tab:shipping/method', [$this, 'add_shipping_method'], 15 );
add_filter( 'formipay/form-settings/tab:shipping', [$this, 'add_shipping_settings'], 15 );
// Add to order details
add_filter( 'formipay/order/order-details', [$this, 'add_shipping_to_order_details'], 99, 3 );
@@ -33,85 +34,66 @@ class FlatRate extends Shipping {
}
/**
* Add flat rate settings to form shipping configuration
* These fields are shown when "Flat Rate" is selected as the shipping method
*/
public function add_shipping_settings($fields) {
// Get global currencies configuration
$global_currencies = get_global_currency_array();
// Basic flat rate fields (type and label)
$flat_rate_fields = array(
$this->shipping_method.'_group' => array(
'type' => 'group_title',
'label' => __( 'Flat Rate Setup', 'formipay' ),
'description' => __( 'Configure flat rate shipping cost for this form', 'formipay' ),
'dependency' => array(
array(
'key' => 'product_type',
'value' => 'physical',
'section' => 'general'
),
array(
'key' => 'shipping_method',
'key' => 'shipping_enabled',
'value' => 'flat_rate'
)
),
'dependencies' => '&&',
'group' => 'started'
),
$this->shipping_method.'_type' => array(
'type' => 'select',
'label' => __( 'Type', 'formipay' ),
'options' => array(
'fixed' => __( 'Fixed', 'formipay' ),
'percentage' => __( 'Percentage', 'formipay' )
'fixed' => __( 'Fixed Amount', 'formipay' ),
'percentage' => __( 'Percentage of Order Total', 'formipay' )
),
'value' => 'fixed',
'dependency' => array(
array(
'key' => 'product_type',
'value' => 'physical',
'section' => 'general'
),
array(
'key' => 'shipping_method',
'key' => 'shipping_enabled',
'value' => 'flat_rate'
)
),
'dependencies' => '&&',
),
$this->shipping_method.'_amount' => array(
'type' => 'number',
'label' => __( 'Amount', 'formipay' ),
'value' => '10',
'dependency' => array(
array(
'key' => 'product_type',
'value' => 'physical',
'section' => 'general'
),
array(
'key' => 'shipping_method',
'value' => 'flat_rate'
)
),
'dependencies' => '&&',
),
$this->shipping_method.'_label' => array(
'type' => 'text',
'label' => __( 'Label', 'formipay' ),
'description' => __( 'This will be shown in Order Review and Order Details', 'formipay' ),
'value' => __( 'Shipping Fee', 'formipay' ),
'dependency' => array(
array(
'key' => 'product_type',
'value' => 'physical',
'section' => 'general'
),
array(
'key' => 'shipping_method',
'value' => 'flat_rate'
)
),
'dependencies' => '&&',
'group' => 'ended'
),
);
// Add per-currency amount fields
foreach ($global_currencies as $currency) {
// Get the currency code (first part of triple) - this is used for meta key suffix
$currency_code = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
$step = ($currency['decimal_digits'] ?? 2) > 0 ? pow(10, -($currency['decimal_digits'] ?? 2)) : 1;
$is_last = ($currency === end($global_currencies));
$flat_rate_fields[$this->shipping_method.'_amount_'.$currency_code] = array(
'type' => 'number',
'label' => sprintf(__( 'Amount (%s)', 'formipay' ), $currency_code),
'description' => $is_last ? __( 'Shipping cost for this form (not per-product)', 'formipay' ) : '',
'step' => $step,
'min' => 0,
'placeholder' => $is_last ? __( 'Enter Amount...', 'formipay' ) : __( 'Auto', 'formipay' ),
'dependency' => array(
'key' => 'shipping_enabled',
'value' => 'flat_rate'
),
'group' => $is_last ? 'ended' : null,
);
}
// Merge fields into the main fields array
foreach($flat_rate_fields as $key => $value){
$fields[$key] = $value;
}
@@ -120,20 +102,59 @@ class FlatRate extends Shipping {
}
/**
* Add shipping cost to order details
*
* @param array $details Order details array
* @param int $form_id Product/form ID
* @param array $order_data Order data from submission
* @return array Updated order details
*/
public function add_shipping_to_order_details( $details, $form_id, $order_data ) {
if( formipay_get_post_meta($form_id, 'product_type') == 'physical' && formipay_get_post_meta($form_id, 'shipping_method')){
if ( formipay_get_post_meta($form_id, 'product_type') == 'physical' && formipay_get_post_meta($form_id, 'shipping_method') == 'flat_rate' ) {
$amount = floatval( formipay_price_format( formipay_get_post_meta( $form_id, 'flat_rate_amount' ) ) );
$flat_rate_type = formipay_get_post_meta($form_id, 'flat_rate_type');
$flat_rate_label = formipay_get_post_meta($form_id, 'flat_rate_label');
if( formipay_get_post_meta($form_id, 'flat_rate_type') == 'percentage' ) {
$price = floatval( formipay_get_post_meta($form_id, 'product_price') );
$calculate = $price * $amount / 100;
// Get the selected currency from request (same way Order class does it)
$currency = isset($_REQUEST['currency']) ? sanitize_text_field( wp_unslash($_REQUEST['currency']) ) : (string) formipay_default_currency('code');
// Get flat rate amount - check for currency-specific first, then fallback to base
$flat_rate_amount = formipay_get_post_meta($form_id, 'flat_rate_amount_' . $currency);
if (empty($flat_rate_amount)) {
$flat_rate_amount = formipay_get_post_meta($form_id, 'flat_rate_amount');
}
$amount = floatval( formipay_price_format($flat_rate_amount) );
// For percentage-based, calculate from actual product price paid
if ( $flat_rate_type == 'percentage' ) {
// Find the actual product price from order details (already currency-aware)
$product_price = 0;
foreach ($details as $item) {
if (isset($item['context']) && $item['context'] == 'product') {
// Use the first product's amount (already in selected currency)
$product_price = floatval($item['amount']);
break;
}
}
// If no product found in details, fallback to lookup by currency
if ($product_price == 0) {
$regular_key = 'setting_product_price_regular_' . $currency;
$sale_key = 'setting_product_price_sale_' . $currency;
$regular_price = formipay_get_post_meta($form_id, $regular_key);
$sale_price = formipay_get_post_meta($form_id, $sale_key);
$product_price = ($sale_price !== '' && $sale_price !== null) ? floatval($sale_price) : floatval($regular_price);
}
$calculate = $product_price * $amount / 100;
$amount = floatval($calculate);
}
$details[] = [
'item' => formipay_get_post_meta($form_id, 'flat_rate_label'),
'item' => $flat_rate_label,
'amount' => $amount,
'subtotal' => $amount
];

View File

@@ -4,6 +4,15 @@ use Formipay\Traits\SingletonTrait;
if ( ! defined( 'ABSPATH' ) ) exit;
/**
* Abstract Shipping Class - Core Carrier Extension System
*
* This class provides the hook system for carrier API extensions.
* Carriers like Rajaongkir, Biteship, etc. can extend Formipay shipping
* by registering themselves via the provided hooks.
*
* @phase 4 - Carrier API Extension System
*/
abstract class Shipping {
use SingletonTrait;
@@ -14,8 +23,42 @@ abstract class Shipping {
protected function __construct() {
// Phase 1-3: Core shipping functionality
add_filter( 'formipay/global-settings', [$this, 'add_setting_shipping_menu'], 15 );
add_filter( 'formipay/product-config', [$this, 'add_form_shipping_menu'], 75 );
add_filter( 'formipay/form-config', [$this, 'add_form_shipping_config'], 75 );
add_filter( 'formipay/global-settings/tab:shipping', [$this, 'add_global_shipping_settings'], 15 );
// Phase 4: Carrier Extension Hooks
// Carrier registration - allows carriers to register themselves
add_filter( 'formipay/shipping/carriers', '__return_empty_array', 5 );
// Carrier API keys - inject into global shipping settings
add_filter( 'formipay/global-settings/tab:shipping', [$this, 'add_carrier_api_settings'], 20 );
// Checkout address fields - inject carrier-specific address fields
add_filter( 'formipay/checkout/shipping-address-fields', [$this, 'get_carrier_address_fields'], 10, 3 );
// Live rate fetching - allow carriers to provide real-time rates
add_filter( 'formipay/shipping/live-rates', '__return_empty_array', 10, 4 );
// AJAX handler for testing carrier connection
add_action( 'wp_ajax_formipay_test_carrier_connection', [$this, 'ajax_test_carrier_connection'], 10 );
add_action( 'wp_ajax_nopriv_formipay_test_carrier_connection', [$this, 'ajax_test_carrier_connection'], 10 );
// Phase 5: Checkout Integration
// AJAX endpoint for getting available shipping methods for checkout
add_action( 'wp_ajax_formipay_get_shipping_methods', [$this, 'ajax_get_shipping_methods'], 10 );
add_action( 'wp_ajax_nopriv_formipay_get_shipping_methods', [$this, 'ajax_get_shipping_methods'], 10 );
// AJAX endpoint for getting supported countries
add_action( 'wp_ajax_formipay_get_supported_countries', [$this, 'ajax_get_supported_countries'], 10 );
add_action( 'wp_ajax_nopriv_formipay_get_supported_countries', [$this, 'ajax_get_supported_countries'], 10 );
// Hook to add shipping cost to cart calculation
add_filter( 'formipay/checkout/cart/calculation', [$this, 'add_shipping_to_cart'], 10, 3 );
// Hook to add shipping data to order submission
add_filter( 'formipay/order/process-data', [$this, 'add_shipping_to_order_data'], 10, 2 );
}
@@ -36,19 +79,212 @@ abstract class Shipping {
}
public function add_form_shipping_menu($fields) {
/**
* Add global shipping settings fields
* This implements Phase 3 of the shipping module: Global Shipping Settings
*/
public function add_global_shipping_settings($fields) {
$shipping_methods = apply_filters( 'formipay/product-settings/tab:shipping/method', [
// Load countries from JSON file
$countries_json = FORMIPAY_PATH . 'admin/assets/json/country.json';
$countries = file_exists($countries_json) ? json_decode(file_get_contents($countries_json), true) : [];
$country_options = [];
if (is_array($countries)) {
foreach ($countries as $country) {
$code = $country['code'] ?? '';
$name = $country['name'] ?? '';
if ($code && $name) {
$country_options[$code] = $name;
}
}
}
// Store Origin Section
$fields['shipping_origin_group'] = array(
'type' => 'group_title',
'label' => __( 'Store Origin', 'formipay' ),
'description' => __( 'Your business location for shipping calculations', 'formipay' ),
'group' => 'started'
);
$fields['shipping_origin_country'] = array(
'type' => 'select',
'label' => __( 'Origin Country', 'formipay' ),
'options' => $country_options,
'searchable' => true,
'description' => __( 'Select the country where your products ship from', 'formipay' ),
);
$fields['shipping_weight_unit'] = array(
'type' => 'select',
'label' => __( 'Weight Unit', 'formipay' ),
'options' => array(
'kg' => __( 'Kilograms (kg)', 'formipay' ),
'g' => __( 'Grams (g)', 'formipay' ),
'lb' => __( 'Pounds (lb)', 'formipay' ),
'oz' => __( 'Ounces (oz)', 'formipay' ),
),
'value' => 'kg',
);
$fields['shipping_dimension_unit'] = array(
'type' => 'select',
'label' => __( 'Dimension Unit', 'formipay' ),
'options' => array(
'cm' => __( 'Centimeters (cm)', 'formipay' ),
'in' => __( 'Inches (in)', 'formipay' ),
'm' => __( 'Meters (m)', 'formipay' ),
),
'value' => 'cm',
'group' => 'ended'
);
// Shipping Calculation Method
$fields['shipping_calculation_group'] = array(
'type' => 'group_title',
'label' => __( 'Shipping Calculation', 'formipay' ),
'description' => __( 'How shipping costs are calculated for orders', 'formipay' ),
'group' => 'started'
);
$fields['shipping_calculation_method'] = array(
'type' => 'select',
'label' => __( 'Calculation Method', 'formipay' ),
'options' => array(
'per_order' => __( 'Per Order (single shipping fee for entire order)', 'formipay' ),
'per_item' => __( 'Per Item (shipping fee multiplied by quantity)', 'formipay' ),
),
'value' => 'per_order',
'group' => 'ended'
);
// Supported Destinations Section
$fields['shipping_destinations_group'] = array(
'type' => 'group_title',
'label' => __( 'Supported Destinations', 'formipay' ),
'description' => __( 'Configure which countries you ship to and their shipping rates', 'formipay' ),
'group' => 'started'
);
// Get enabled currencies for flat rate table
$formipay_settings = get_option('formipay_settings', []);
$enabled_currencies = [];
if (!empty($formipay_settings['multicurrencies']) && is_array($formipay_settings['multicurrencies'])) {
foreach ($formipay_settings['multicurrencies'] as $currency) {
if (isset($currency['currency'])) {
$parts = explode(':::', $currency['currency']);
$enabled_currencies[] = [
'code' => $parts[0] ?? '',
'title' => $parts[1] ?? '',
'symbol' => $parts[2] ?? $parts[0] ?? '',
];
}
}
}
// Fallback to default currency if multicurrency is not enabled
if (empty($enabled_currencies)) {
$default_currency = $formipay_settings['default_currency'] ?? 'IDR:::Indonesian rupiah:::Rp';
$parts = explode(':::', $default_currency);
$enabled_currencies[] = [
'code' => $parts[0] ?? 'IDR',
'title' => $parts[1] ?? 'Indonesian rupiah',
'symbol' => $parts[2] ?? 'Rp',
];
}
// Build currency amount fields for the repeater
$currency_fields = [];
foreach ($enabled_currencies as $curr) {
$code = $curr['code'];
$symbol = $curr['symbol'];
$currency_fields['flat_rate_' . $code] = array(
'type' => 'number',
'label' => sprintf(__( 'Flat Rate (%s)', 'formipay' ), $code),
'step' => 0.01,
'min' => 0,
'placeholder' => '0.00',
);
}
// Build free shipping threshold field (use primary currency)
$primary_currency = $enabled_currencies[0] ?? [];
$primary_symbol = $primary_currency['symbol'] ?? '';
$fields['shipping_destinations'] = array(
'type' => 'repeater',
'label' => __( 'Destinations', 'formipay' ),
'description' => __( 'Add countries you ship to and configure their shipping options', 'formipay' ),
'fields' => array_merge(
[
'country' => array(
'type' => 'select',
'label' => __('Country', 'formipay'),
'options' => $country_options,
'required' => true,
'searchable' => true,
'is_group_title' => true
),
'rate_source' => array(
'type' => 'select',
'label' => __('Rate Source', 'formipay'),
'options' => array(
'flat_rate' => __( 'Flat Rate', 'formipay' ),
// 'api' => __( 'Carrier API', 'formipay' ), // Phase 4
),
'value' => 'flat_rate',
),
],
$currency_fields,
[
'free_shipping_threshold' => array(
'type' => 'number',
'label' => sprintf(__( 'Free Shipping Threshold (%s)', 'formipay' ), $primary_symbol),
'description' => __( 'Order amount above which shipping is free. Leave empty to disable.', 'formipay' ),
'step' => 0.01,
'min' => 0,
'placeholder' => 'Empty = disabled',
),
]
)
);
// Note: Carrier API settings will be added in Phase 4
$fields['carrier_api_note'] = array(
'type' => 'notification_message',
'description' => __( '
<h3>Carrier API Integration</h3>
<p>Live carrier rates (Rajaongkir, Biteship, etc.) will be available in Phase 4 of the shipping module.</p>
<p>Currently, only Flat Rate and Free Shipping methods are available at the product level.</p>
', 'formipay' ),
);
return $fields;
}
/**
* Add shipping configuration to form settings
* This replaces product-level shipping with form-level shipping
*/
public function add_form_shipping_config($fields) {
$shipping_methods = apply_filters( 'formipay/form-settings/tab:shipping/method', [
'no_shipping' => [
'method' => __( 'No Shipping Required', 'formipay' )
],
'flat_rate' => [
'method' => __( 'Flat Rate', 'formipay' )
],
'free_shipping' => [
'method' => __( 'Free Shipping', 'formipay' )
]
] );
$shipping_options = [];
$shipping_fields = [];
foreach($shipping_methods as $id => $shipping){
// $id = $shipping['id'];
$label = $shipping['method'];
if(isset($shipping['courier'])){
$label .= ' - '.$shipping['courier'];
@@ -59,102 +295,440 @@ abstract class Shipping {
$shipping_options[$id] = $label;
}
$shipping_fields = [
'shipping_notice' => array(
'type' => 'notification_message',
'image' => FORMIPAY_URL . 'admin/assets/img/logistics.png',
'description' => __( '
<h1>No Shipping Method Available</h1>
<p>Shipping methods only for physical product type. If you insist to use shipping method, change your product type first</p>
', 'formipay' ),
'dependency' => array(
'key' => 'product_type',
'value' => 'digital',
'section' => 'general'
),
),
'shipping_method' => array(
// Main shipping configuration group
$shipping_config_group = [
'shipping_enable_group' => [
'type' => 'group_title',
'label' => __( 'Shipping Configuration', 'formipay' ),
'description' => __( 'Configure shipping options for this form. Shipping will be calculated based on form settings, not per-product.', 'formipay' ),
'group' => 'started'
],
'shipping_enabled' => [
'type' => 'radio',
'label' => esc_html__('Shipping Methods', 'formipay'),
'label' => __( 'Shipping Method', 'formipay' ),
'options' => $shipping_options,
'dependency' => array(
'key' => 'product_type',
'value' => 'physical',
'section' => 'general'
),
)
'value' => 'no_shipping',
'description' => __( 'Select how shipping should be handled for orders from this form', 'formipay' ),
]
];
$free_shipping_fields = array(
'free_shipping_group' => array(
'type' => 'group_title',
'label' => __( 'Free Shipping Setup', 'formipay' ),
'description' => __( 'Will not add any shipping fee to the order', 'formipay' ),
'dependency' => array(
array(
'key' => 'product_type',
'value' => 'physical',
'section' => 'general'
),
array(
'key' => 'shipping_method',
'value' => 'free_shipping'
)
),
'dependencies' => '&&',
'group' => 'started'
),
'free_shipping_label' => array(
'type' => 'text',
'label' => __( 'Label', 'formipay' ),
'value' => __( 'Free Shipping', 'formipay' ),
'dependency' => array(
array(
'key' => 'product_type',
'value' => 'physical',
'section' => 'general'
),
array(
'key' => 'shipping_method',
'value' => 'free_shipping'
)
),
'dependencies' => '&&',
),
'free_shipping_add_to_order_review' => array(
'type' => 'checkbox',
'label' => __( 'Show in Order Review', 'formipay' ),
'dependency' => array(
array(
'key' => 'product_type',
'value' => 'physical',
'section' => 'general'
),
array(
'key' => 'shipping_method',
'value' => 'free_shipping'
)
),
'dependencies' => '&&',
'group' => 'ended'
),
);
$shipping_config_group = apply_filters( 'formipay/form-settings/tab:shipping/group:config', $shipping_config_group );
$last_config_key = array_key_last($shipping_config_group);
$shipping_config_group[$last_config_key]['group'] = 'ended';
// Apply carrier-specific settings (Flat Rate, etc.)
$carrier_settings = apply_filters( 'formipay/form-settings/tab:shipping', [] );
$all_shipping_fields = array_merge($shipping_config_group, $carrier_settings);
$fields['formipay_form_settings']['shipping'] = [
'name' => __( 'Shipping', 'formipay' ),
'fields' => $all_shipping_fields
];
return $fields;
foreach($free_shipping_fields as $key => $value) {
$shipping_fields[$key] = $value;
}
$shipping_fields = apply_filters( 'formipay/product-settings/tab:shipping', $shipping_fields );
/**
* =============================================
* PHASE 4: CARRIER EXTENSION HOOKS
* =============================================
*/
if(!empty($shipping_fields)){
$fields['formipay_product_settings']['shipping'] = array(
'name' => __( 'Shipping', 'formipay' ),
'fields' => $shipping_fields
/**
* Add carrier API settings to global shipping settings
* Carriers can hook into `formipay/global-settings/tab:shipping/carriers` to add their API key fields
*
* @param array $fields Existing shipping settings fields
* @return array Updated fields with carrier API settings
*/
public function add_carrier_api_settings($fields) {
// Get all registered carriers
$carriers = apply_filters('formipay/shipping/carriers', []);
if (empty($carriers)) {
// No carriers registered, add info message
$fields['carrier_api_info'] = array(
'type' => 'notification_message',
'description' => __( '
<h3>Carrier API Integration</h3>
<p>To enable live shipping rates, install a carrier extension plugin.</p>
<p>Extensions register themselves via the <code>formipay/shipping/carriers</code> filter.</p>
', 'formipay' ),
);
return $fields;
}
// Add carrier API settings section
$fields['carrier_api_group'] = array(
'type' => 'group_title',
'label' => __( 'Carrier API Keys', 'formipay' ),
'description' => __( 'Configure API credentials for live shipping rate calculation', 'formipay' ),
'group' => 'started'
);
// Allow carriers to inject their API key fields
$carrier_fields = apply_filters('formipay/global-settings/tab:shipping/carriers', []);
foreach ($carrier_fields as $key => $field) {
$fields[$key] = $field;
}
// Add test connection buttons for each carrier
foreach ($carriers as $carrier_id => $carrier) {
if (isset($carrier['test_connection']) && $carrier['test_connection']) {
$fields['test_connection_' . $carrier_id] = array(
'type' => 'html',
'label' => __( 'Test Connection', 'formipay' ),
'html' => sprintf(
'<button type="button" class="button formipay-test-connection" data-carrier="%s">%s</button>
<span class="formipay-connection-result" style="margin-left: 10px;"></span>',
esc_attr($carrier_id),
esc_html__('Test Connection', 'formipay')
),
'group' => ($carrier_id === array_key_last($carriers)) ? 'ended' : null,
);
}
}
return $fields;
}
/**
* Get carrier-specific address fields for checkout
* Carriers can hook into `formipay/checkout/address-fields/{carrier_id}` to provide their fields
*
* @param array $fields Current address fields
* @param string $carrier_id Carrier identifier (e.g., 'rajaongkir', 'biteship')
* @param string $country_code Destination country code
* @return array Address fields for this carrier
*/
public function get_carrier_address_fields($fields, $carrier_id, $country_code) {
// Get carrier-specific address fields
$carrier_fields = apply_filters('formipay/checkout/address-fields/' . $carrier_id, [], $country_code);
return array_merge($fields, $carrier_fields);
}
/**
* AJAX handler for testing carrier connection
* Carriers can hook into `formipay/test_carrier_connection/{carrier_id}` to handle the test
*/
public function ajax_test_carrier_connection() {
check_ajax_referer('formipay-admin', 'nonce', true);
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => __('Unauthorized', 'formipay')]);
}
$carrier_id = isset($_POST['carrier']) ? sanitize_text_field(wp_unslash($_POST['carrier'])) : '';
if (empty($carrier_id)) {
wp_send_json_error(['message' => __('Missing carrier ID', 'formipay')]);
}
// Allow carriers to handle their own connection test
$result = apply_filters('formipay/test_carrier_connection/' . $carrier_id, [
'success' => false,
'message' => __('Carrier does not implement connection test', 'formipay'),
], $_POST);
if ($result['success']) {
wp_send_json_success($result);
} else {
wp_send_json_error($result);
}
}
/**
* Helper method: Get registered carriers
* Returns all carriers that have registered via the filter
*
* @return array Registered carriers
*/
public static function get_registered_carriers() {
return apply_filters('formipay/shipping/carriers', []);
}
/**
* Helper method: Get carrier by ID
*
* @param string $carrier_id Carrier identifier
* @return array|null Carrier data or null if not found
*/
public static function get_carrier($carrier_id) {
$carriers = self::get_registered_carriers();
return $carriers[$carrier_id] ?? null;
}
/**
* Helper method: Check if carrier supports a country
*
* @param string $carrier_id Carrier identifier
* @param string $country_code Country code to check
* @return bool True if carrier supports the country
*/
public static function carrier_supports_country($carrier_id, $country_code) {
$carrier = self::get_carrier($carrier_id);
if (!$carrier) {
return false;
}
$supported_countries = $carrier['countries'] ?? [];
return in_array($country_code, $supported_countries, true);
}
/**
* Helper method: Fetch live rates from carrier
*
* @param string $carrier_id Carrier identifier
* @param array $params Rate request parameters (origin, destination, weight, dimensions)
* @return array Available rates with costs
*/
public static function fetch_live_rates($carrier_id, $params) {
$default_params = [
'origin_country' => '',
'origin_city' => '',
'origin_postcode' => '',
'destination_country' => '',
'destination_city' => '',
'destination_postcode' => '',
'weight' => 0,
'weight_unit' => 'kg',
'length' => 0,
'width' => 0,
'height' => 0,
'dimension_unit' => 'cm',
];
$params = wp_parse_args($params, $default_params);
return apply_filters('formipay/shipping/live-rates', [], $carrier_id, $params);
}
/**
* =============================================
* PHASE 5: CHECKOUT INTEGRATION
* =============================================
*/
/**
* AJAX: Get available shipping methods for a form
* Now reads from form-level shipping settings instead of global settings
*/
public function ajax_get_shipping_methods() {
check_ajax_referer('formipay-public', 'nonce', true);
$form_id = isset($_POST['form_id']) ? intval($_POST['form_id']) : 0;
$country_code = isset($_POST['country']) ? sanitize_text_field(wp_unslash($_POST['country'])) : '';
$currency = isset($_POST['currency']) ? sanitize_text_field(wp_unslash($_POST['currency'])) : '';
if (!$form_id) {
wp_send_json_error(['message' => __('Invalid request', 'formipay')]);
}
// Get form shipping settings
$form_settings = get_post_meta($form_id, 'formipay_form_settings', true);
$shipping_enabled = $form_settings['shipping_enabled'] ?? 'no_shipping';
$available_methods = [];
if ($shipping_enabled === 'flat_rate') {
// Get flat rate from form settings
$currency_code = $currency ?: 'IDR';
$rate_key = 'flat_rate_amount_' . $currency_code;
$flat_rate = floatval($form_settings[$rate_key] ?? 0);
$flat_rate_type = $form_settings['flat_rate_type'] ?? 'fixed';
// For percentage, we'll calculate on frontend based on cart total
// For now, store the type so frontend knows how to handle it
$available_methods[] = [
'id' => 'flat_rate',
'name' => __('Standard Shipping', 'formipay'),
'description' => $flat_rate_type === 'percentage'
? sprintf(__('%s%% of order total', 'formipay'), $flat_rate)
: __('Delivery in 3-5 business days', 'formipay'),
'cost' => $flat_rate,
'currency' => $currency_code,
'type' => $flat_rate_type,
];
} elseif ($shipping_enabled === 'free_shipping') {
// Free shipping from form settings
$free_label = $form_settings['free_shipping_label'] ?? __('Free Shipping', 'formipay');
$available_methods[] = [
'id' => 'free_shipping',
'name' => $free_label,
'description' => __('No shipping cost', 'formipay'),
'cost' => 0,
'currency' => $currency_code ?? '',
];
}
// If country-specific shipping is needed in future, add check here
// For now, form-level shipping applies to all countries
if (empty($available_methods)) {
wp_send_json_error([
'message' => __('Shipping is not available for this form', 'formipay'),
'methods' => []
]);
}
wp_send_json_success([
'methods' => $available_methods,
'default_method' => $available_methods[0]['id'],
]);
}
/**
* Add shipping cost to cart calculation
* Hooked into formipay/checkout/cart/calculation
*/
public function add_shipping_to_cart($cart, $form_id, $selected_currency) {
// Check if shipping is enabled for this form
$form_settings = get_post_meta($form_id, 'formipay_form_settings', true);
$shipping_enabled = $form_settings['shipping_enabled'] ?? 'no_shipping';
if ($shipping_enabled === 'no_shipping') {
return $cart;
}
// Get selected shipping method from POST data or session
$shipping_method = isset($_POST['shipping_method']) ? sanitize_text_field(wp_unslash($_POST['shipping_method'])) : '';
$shipping_country = isset($_POST['shipping_country']) ? sanitize_text_field(wp_unslash($_POST['shipping_country'])) : '';
if (empty($shipping_method) || empty($shipping_country)) {
return $cart;
}
// Parse shipping method ID to get cost
// Format: flat_rate, free_shipping, or {carrier}_{service}
if ($shipping_method === 'free_shipping') {
// Free shipping
$cart['shipping'] = [
'name' => __('Free Shipping', 'formipay'),
'cost' => 0,
];
} elseif ($shipping_method === 'flat_rate') {
// Flat rate - get cost from form settings
$currency_code = $selected_currency ?: 'IDR';
$rate_key = 'flat_rate_amount_' . $currency_code;
$flat_rate = floatval($form_settings[$rate_key] ?? 0);
// Check if percentage
$flat_rate_type = $form_settings['flat_rate_type'] ?? 'fixed';
if ($flat_rate_type === 'percentage') {
$subtotal = floatval($cart['subtotal'] ?? 0);
$flat_rate = ($subtotal * $flat_rate) / 100;
}
$cart['shipping'] = [
'name' => __('Standard Shipping', 'formipay'),
'cost' => $flat_rate,
];
// Recalculate totals
$cart['subtotal'] = floatval($cart['subtotal'] ?? 0);
$cart['tax'] = floatval($cart['tax'] ?? 0);
$cart['discount'] = floatval($cart['discount'] ?? 0);
$cart['grand'] = $cart['subtotal'] + $cart['tax'] + $cart['shipping']['cost'] - $cart['discount'];
}
return $cart;
}
/**
* Add shipping data to order submission
* Hooked into formipay/order/process-data
*/
public function add_shipping_to_order_data($form_data, $form_id) {
// Check if shipping is enabled for this form
$form_settings = get_post_meta($form_id, 'formipay_form_settings', true);
$shipping_enabled = $form_settings['shipping_enabled'] ?? 'no_shipping';
if ($shipping_enabled === 'no_shipping') {
return $form_data;
}
// Add shipping info to form data
if (isset($_POST['shipping_method'])) {
$form_data['shipping_method'] = sanitize_text_field(wp_unslash($_POST['shipping_method']));
}
if (isset($_POST['shipping_country'])) {
$form_data['shipping_country'] = sanitize_text_field(wp_unslash($_POST['shipping_country']));
}
return $form_data;
}
/**
* AJAX: Get supported shipping countries
* With form-level shipping, returns all available countries
*/
public function ajax_get_supported_countries() {
// Verify nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'formipay_public_nonce')) {
wp_send_json_error(['message' => __('Invalid security token', 'formipay')]);
}
$form_id = isset($_POST['form_id']) ? intval($_POST['form_id']) : 0;
if (!$form_id) {
wp_send_json_error(['message' => __('Invalid form ID', 'formipay')]);
}
// Load countries from JSON file
$countries_json = FORMIPAY_PATH . 'admin/assets/json/country.json';
$all_countries = file_exists($countries_json) ? json_decode(file_get_contents($countries_json), true) : [];
// Build country list
$countries = [];
if (is_array($all_countries)) {
foreach ($all_countries as $country) {
$code = $country['code'] ?? '';
$name = $country['name'] ?? '';
if ($code && $name) {
$countries[$code] = $name;
}
}
}
if (empty($countries)) {
wp_send_json_error([
'message' => __('No countries available', 'formipay'),
'countries' => []
]);
}
wp_send_json_success([
'countries' => $countries,
]);
}
}

View 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);

File diff suppressed because it is too large Load Diff