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

@@ -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,19 +86,26 @@ class Product {
public function add_submenu() {
add_submenu_page(
'formipay',
__( 'Products', 'formipay' ),
__( 'Products', 'formipay' ),
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'],
'formipay-products',
[$this, 'formipay_product'],
2
);
add_submenu_page(
'formipay',
__('Categories', 'formipay'),
'└ ' . __('Categories', 'formipay'),
'manage_options',
add_submenu_page(
'formipay',
__('Categories', 'formipay'),
'└ ' . __('Categories', 'formipay'),
'manage_options',
'edit-tags.php?taxonomy=formipay-product-category&post_type=formipay-product',
null,
5
@@ -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) {
@@ -242,13 +316,112 @@ class Product {
}
public function cpt_post_fields_content($fields) {
$fields['formipay_product_settings'] = array();
$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)