From c6489b6b05ed6272dddd2deb6dce28d2e9ba569e Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Thu, 8 Jan 2026 23:41:30 +0700 Subject: [PATCH] fix: shipping cost applied to orders + dynamic field rendering Shipping Fix: - Frontend now sends shipping_cost and shipping_title in order payload - Backend uses these values as fallback when WC zone-based rate lookup fails - Fixes issue where Rajaongkir and other API-based shipping wasn't applied Dynamic Field Rendering: - Added billingFields/shippingFields filters sorted by priority - Added getBillingField/getShippingField helpers that return undefined for hidden fields - All standard fields now conditionally rendered based on API response - Fields use labels and required flags from API - Any field can be hidden via PHP snippet (type: 'hidden' or hidden: true) - Removed unused isFieldHidden function --- customer-spa/src/pages/Checkout/index.tsx | 202 ++++++++++++---------- includes/Api/CheckoutController.php | 17 ++ 2 files changed, 131 insertions(+), 88 deletions(-) diff --git a/customer-spa/src/pages/Checkout/index.tsx b/customer-spa/src/pages/Checkout/index.tsx index 1b9720b..91b1906 100644 --- a/customer-spa/src/pages/Checkout/index.tsx +++ b/customer-spa/src/pages/Checkout/index.tsx @@ -222,16 +222,27 @@ export default function Checkout() { loadCheckoutFields(); }, [cart.items, isVirtualOnly]); - // Helper to check if a standard field should be hidden (set to type: 'hidden' or hidden: true in PHP) - const isFieldHidden = (fieldKey: string): boolean => { - const field = checkoutFields.find(f => f.key === fieldKey); - // Check both type='hidden' and the hidden flag from API - return field?.type === 'hidden' || field?.hidden === true; - }; + // NOTE: Old isFieldHidden function removed - now using getBillingField/getShippingField + // which return undefined for hidden fields (type: 'hidden' or hidden: true) - // Filter custom fields by fieldset - const billingCustomFields = checkoutFields.filter(f => f.fieldset === 'billing' && f.custom && !f.hidden); - const shippingCustomFields = checkoutFields.filter(f => f.fieldset === 'shipping' && f.custom && !f.hidden); + // Get all billing fields from API (standard + custom), filtered and sorted by priority + // This allows ANY field to be hidden via PHP (type: 'hidden' or hidden: true) + const billingFields = checkoutFields + .filter(f => f.fieldset === 'billing' && f.type !== 'hidden' && !f.hidden) + .sort((a, b) => (a.priority || 10) - (b.priority || 10)); + + // Get all shipping fields from API (standard + custom), filtered and sorted by priority + const shippingFields = checkoutFields + .filter(f => f.fieldset === 'shipping' && f.type !== 'hidden' && !f.hidden) + .sort((a, b) => (a.priority || 10) - (b.priority || 10)); + + // Helper to get a billing field from API by key (for checking if it should be rendered) + const getBillingField = (key: string) => billingFields.find(f => f.key === key); + const getShippingField = (key: string) => shippingFields.find(f => f.key === key); + + // Filter custom fields by fieldset (legacy support - for plugins that add non-standard fields) + const billingCustomFields = checkoutFields.filter(f => f.fieldset === 'billing' && f.custom && !f.hidden && f.type !== 'hidden'); + const shippingCustomFields = checkoutFields.filter(f => f.fieldset === 'shipping' && f.custom && !f.hidden && f.type !== 'hidden'); // Handler for custom field changes const handleCustomFieldChange = (key: string, value: string) => { @@ -533,6 +544,9 @@ export default function Checkout() { }, payment_method: paymentMethod, shipping_method: selectedShippingRate || undefined, // Selected shipping rate ID + // Also send shipping cost/title for direct use when WC rate lookup fails (API-based shipping like Rajaongkir) + shipping_cost: shippingCost, + shipping_title: shippingRates.find(r => r.id === selectedShippingRate)?.label || '', coupons: appliedCoupons.map(c => c.code), // Send applied coupon codes customer_note: orderNotes, // Include all custom field data for backend processing @@ -671,77 +685,89 @@ export default function Checkout() {

Billing Details

-
- - setBillingData({ ...billingData, firstName: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setBillingData({ ...billingData, lastName: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setBillingData({ ...billingData, email: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
-
- - setBillingData({ ...billingData, phone: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
+ {/* All fields conditionally rendered based on API response */} + {/* This allows ANY field to be hidden via PHP snippet */} + {getBillingField('billing_first_name') && ( +
+ + setBillingData({ ...billingData, firstName: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+ )} + {getBillingField('billing_last_name') && ( +
+ + setBillingData({ ...billingData, lastName: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+ )} + {getBillingField('billing_email') && ( +
+ + setBillingData({ ...billingData, email: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+ )} + {getBillingField('billing_phone') && ( +
+ + setBillingData({ ...billingData, phone: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+ )} {/* Address fields - only for physical products */} {!isVirtualOnly && ( <> -
- - setBillingData({ ...billingData, address: e.target.value })} - className="w-full border rounded-lg px-4 py-2" - /> -
- {/* City field - hidden if PHP sets type to 'hidden' */} - {!isFieldHidden('billing_city') && ( -
- + {getBillingField('billing_address_1') && ( +
+ setBillingData({ ...billingData, address: e.target.value })} + className="w-full border rounded-lg px-4 py-2" + /> +
+ )} + {/* City field - conditionally rendered based on API */} + {getBillingField('billing_city') && ( +
+ + setBillingData({ ...billingData, city: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
)} - {/* Country field - hidden if PHP sets type to 'hidden' */} - {!isFieldHidden('billing_country') && ( + {/* Country field - conditionally rendered based on API */} + {getBillingField('billing_country') && (
- +
)} - {/* State field - hidden if PHP sets type to 'hidden' */} - {!isFieldHidden('billing_state') && ( + {/* State field - conditionally rendered based on API */} + {getBillingField('billing_state') && (
- + {billingStateOptions.length > 0 ? ( )} - {/* Postcode field - hidden if PHP sets type to 'hidden' */} - {!isFieldHidden('billing_postcode') && ( + {/* Postcode field - conditionally rendered based on API */} + {getBillingField('billing_postcode') && (
- + setBillingData({ ...billingData, postcode: e.target.value })} className="w-full border rounded-lg px-4 py-2" @@ -919,23 +945,23 @@ export default function Checkout() { className="w-full border rounded-lg px-4 py-2" />
- {/* City field - hidden if PHP sets type to 'hidden' */} - {!isFieldHidden('shipping_city') && ( + {/* City field - conditionally rendered based on API */} + {getShippingField('shipping_city') && (
- + setShippingData({ ...shippingData, city: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
)} - {/* Country field - hidden if PHP sets type to 'hidden' */} - {!isFieldHidden('shipping_country') && ( + {/* Country field - conditionally rendered based on API */} + {getShippingField('shipping_country') && (
- +
)} - {/* State field - hidden if PHP sets type to 'hidden' */} - {!isFieldHidden('shipping_state') && ( + {/* State field - conditionally rendered based on API */} + {getShippingField('shipping_state') && (
- + {shippingStateOptions.length > 0 ? ( )} - {/* Postcode field - hidden if PHP sets type to 'hidden' */} - {!isFieldHidden('shipping_postcode') && ( + {/* Postcode field - conditionally rendered based on API */} + {getShippingField('shipping_postcode') && (
- + setShippingData({ ...shippingData, postcode: e.target.value })} className="w-full border rounded-lg px-4 py-2" diff --git a/includes/Api/CheckoutController.php b/includes/Api/CheckoutController.php index a582c6d..8d933a2 100644 --- a/includes/Api/CheckoutController.php +++ b/includes/Api/CheckoutController.php @@ -426,6 +426,23 @@ class CheckoutController { 'taxes' => $rate->get_taxes(), ]); $order->add_item($item); + } elseif (!empty($payload['shipping_cost']) && $payload['shipping_cost'] > 0) { + // Fallback: use shipping_cost directly from frontend + // This handles API-based shipping like Rajaongkir where WC zones don't apply + $item = new \WC_Order_Item_Shipping(); + + // Parse method ID from shipping_method (format: "method_id:instance_id" or "method_id:instance_id:variant") + $parts = explode(':', $payload['shipping_method']); + $method_id = $parts[0] ?? 'shipping'; + $instance_id = isset($parts[1]) ? (int)$parts[1] : 0; + + $item->set_props([ + 'method_title' => sanitize_text_field($payload['shipping_title'] ?? 'Shipping'), + 'method_id' => sanitize_text_field($method_id), + 'instance_id' => $instance_id, + 'total' => floatval($payload['shipping_cost']), + ]); + $order->add_item($item); } }