From fb1a6c40efefac432475f7cb969c3d09abb42088 Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Wed, 3 Jun 2026 22:56:02 +0700 Subject: [PATCH] Fix RajaOngkir address integration bugs and styling issues --- .../src/components/AddressSelector.tsx | 25 +++-- .../src/components/DynamicCheckoutField.tsx | 10 +- customer-spa/src/pages/Account/Addresses.tsx | 17 ++- customer-spa/src/pages/Checkout/index.tsx | 35 +++++- includes/Api/CheckoutController.php | 104 ++++++++++++++++++ includes/Frontend/AddressController.php | 4 + snippets/rajaongkir-x-woonoow.php | 89 ++++++++++++++- 7 files changed, 263 insertions(+), 21 deletions(-) diff --git a/customer-spa/src/components/AddressSelector.tsx b/customer-spa/src/components/AddressSelector.tsx index 3411a99..529d6bb 100644 --- a/customer-spa/src/components/AddressSelector.tsx +++ b/customer-spa/src/components/AddressSelector.tsx @@ -18,6 +18,7 @@ interface Address { email?: string; phone?: string; is_default: boolean; + formatted_address?: string; } interface AddressSelectorProps { @@ -148,14 +149,22 @@ export function AddressSelector({ )} {/* Address */} -

- {address.address_1} - {address.address_2 && `, ${address.address_2}`} -

-

- {address.city}, {address.state} {address.postcode} -

-

{address.country}

+
+ {address.formatted_address ? ( +

{address.formatted_address}

+ ) : ( + <> +

+ {address.address_1} + {address.address_2 && `, ${address.address_2}`} +

+

+ {address.city}, {address.state} {address.postcode} +

+

{address.country}

+ + )} +
))} diff --git a/customer-spa/src/components/DynamicCheckoutField.tsx b/customer-spa/src/components/DynamicCheckoutField.tsx index 0107a0f..80e6085 100644 --- a/customer-spa/src/components/DynamicCheckoutField.tsx +++ b/customer-spa/src/components/DynamicCheckoutField.tsx @@ -28,6 +28,7 @@ interface CheckoutField { interface DynamicCheckoutFieldProps { field: CheckoutField; value: string; + valueLabel?: string; onChange: (value: string) => void; countryOptions?: { value: string; label: string }[]; stateOptions?: { value: string; label: string }[]; @@ -41,6 +42,7 @@ interface SearchOption { export function DynamicCheckoutField({ field, value, + valueLabel, onChange, countryOptions = [], stateOptions = [], @@ -54,9 +56,11 @@ export function DynamicCheckoutField({ return; } - // If we have a value but no options yet, we might need to load it - // This handles pre-selected values - }, [field.type, field.search_endpoint, value]); + // If we have a value and a label, inject it into searchOptions so it renders properly when mounted + if (value && valueLabel && searchOptions.length === 0) { + setSearchOptions([{ value, label: valueLabel }]); + } + }, [field.type, field.search_endpoint, value, valueLabel]); // Handle API search for searchable_select const handleApiSearch = async (searchTerm: string) => { diff --git a/customer-spa/src/pages/Account/Addresses.tsx b/customer-spa/src/pages/Account/Addresses.tsx index 41c0bf7..879b6fd 100644 --- a/customer-spa/src/pages/Account/Addresses.tsx +++ b/customer-spa/src/pages/Account/Addresses.tsx @@ -345,10 +345,18 @@ export default function Addresses() {

{address.first_name} {address.last_name}

{address.company &&

{address.company}

} -

{address.address_1}

- {address.address_2 &&

{address.address_2}

} -

{address.city}, {address.state} {address.postcode}

-

{address.country}

+ {address.formatted_address ? ( +

{address.formatted_address}

+ ) : ( + <> +

{address.address_1}

+ {address.address_2 &&

{address.address_2}

} + {[address.city, address.state, address.postcode].filter(Boolean).length > 0 && ( +

{[address.city, address.state, address.postcode].filter(Boolean).join(', ')}

+ )} +

{address.country}

+ + )} {address.phone &&

Phone: {address.phone}

} {address.email &&

Email: {address.email}

}
@@ -429,6 +437,7 @@ export default function Addresses() { key={field.key} field={field} value={getFieldValue(field.key)} + valueLabel={getFieldValue(field.key + '_label')} onChange={(v) => setFieldValue(field.key, v)} countryOptions={countryOptions} stateOptions={stateOptions} diff --git a/customer-spa/src/pages/Checkout/index.tsx b/customer-spa/src/pages/Checkout/index.tsx index 689baad..9ee4bdd 100644 --- a/customer-spa/src/pages/Checkout/index.tsx +++ b/customer-spa/src/pages/Checkout/index.tsx @@ -33,6 +33,7 @@ interface SavedAddress { email?: string; phone?: string; is_default: boolean; + formatted_address?: string; } export default function Checkout() { @@ -389,7 +390,13 @@ export default function Checkout() { state: addressData.state, city: addressData.city, postcode: addressData.postcode, - destination_id: undefined, + // Include custom fields for shipping calculation (e.g., RajaOngkir destination_id) + ...Object.fromEntries( + (shipToDifferentAddress ? shippingCustomFields : billingCustomFields).map(f => [ + f.key.replace(/^(shipping_|billing_)/, ''), + customFieldData[f.key] || '' + ]) + ), }, items, }); @@ -795,7 +802,16 @@ export default function Checkout() {

{sel.label}{sel.is_default && Default}

{sel.first_name} {sel.last_name}

-

{sel.address_1}, {sel.city}, {sel.state} {sel.postcode}

+ {sel.formatted_address ? ( +

{sel.formatted_address}

+ ) : ( +
+

{sel.address_1}

+ {[sel.city, sel.state, sel.postcode].filter(Boolean).length > 0 && ( +

{[sel.city, sel.state, sel.postcode].filter(Boolean).join(', ')}

+ )} +
+ )}
) : null; })()} @@ -866,7 +882,7 @@ export default function Checkout() { )} {billingCustomFields.map(field => ( - handleCustomFieldChange(field.key, v)} countryOptions={countryOptions} stateOptions={billingStateOptions} /> + handleCustomFieldChange(field.key, v)} countryOptions={countryOptions} stateOptions={billingStateOptions} /> ))} )} @@ -908,7 +924,16 @@ export default function Checkout() {

{sel.label}

{sel.first_name} {sel.last_name}

-

{sel.address_1}, {sel.city} {sel.postcode}

+ {sel.formatted_address ? ( +

{sel.formatted_address}

+ ) : ( +
+

{sel.address_1}

+ {[sel.city, sel.state, sel.postcode].filter(Boolean).length > 0 && ( +

{[sel.city, sel.state, sel.postcode].filter(Boolean).join(', ')}

+ )} +
+ )}
) : null; })()} @@ -926,7 +951,7 @@ export default function Checkout() { {getShippingField('shipping_country') &&
setShippingData({ ...shippingData, country: v })} placeholder="Select country" disabled={countries.length === 1} />
} {getShippingField('shipping_state') &&
{shippingStateOptions.length > 0 ? setShippingData({ ...shippingData, state: v })} placeholder="Select state" /> : setShippingData({ ...shippingData, state: e.target.value })} className="w-full border rounded-lg px-4 py-2" />}
} {getShippingField('shipping_postcode') &&
setShippingData({ ...shippingData, postcode: e.target.value })} className="w-full border rounded-lg px-4 py-2" />
} - {shippingCustomFields.map(field => handleCustomFieldChange(field.key, v)} countryOptions={countryOptions} stateOptions={shippingStateOptions} />)} + {shippingCustomFields.map(field => handleCustomFieldChange(field.key, v)} countryOptions={countryOptions} stateOptions={shippingStateOptions} />)} )} diff --git a/includes/Api/CheckoutController.php b/includes/Api/CheckoutController.php index 8edadbd..229b157 100644 --- a/includes/Api/CheckoutController.php +++ b/includes/Api/CheckoutController.php @@ -538,6 +538,9 @@ class CheckoutController header('Server-Timing: app;dur=' . round((microtime(true) - $__t0) * 1000, 1)); } + // Auto-save checkout addresses to user account + $this->auto_save_checkout_addresses($order, $payload); + // Clear WooCommerce cart after successful order placement // This ensures the cart page won't re-populate from server session if (function_exists('WC') && WC()->cart) { @@ -1141,4 +1144,105 @@ class CheckoutController } return $results; } + + /** + * Auto-save checkout addresses to the user's address book if they are new. + */ + private function auto_save_checkout_addresses(\WC_Order $order, array $payload): void + { + $user_id = $order->get_customer_id(); + if (!$user_id) { + return; + } + + $addresses = get_user_meta($user_id, 'woonoow_addresses', true); + if (!is_array($addresses)) { + $addresses = []; + } + + // Helper to check if address matches existing + $is_duplicate = function ($new_addr, $type) use ($addresses) { + foreach ($addresses as $addr) { + if ($addr['type'] !== $type && $addr['type'] !== 'both') { + continue; + } + // Compare essential fields + $match = true; + $check_fields = ['first_name', 'last_name', 'address_1', 'city', 'country']; + foreach ($check_fields as $f) { + $v1 = trim(strtolower((string)($addr[$f] ?? ''))); + $v2 = trim(strtolower((string)($new_addr[$f] ?? ''))); + if ($v1 !== $v2) { + $match = false; + break; + } + } + if ($match) return true; + } + return false; + }; + + // Helper to build address array + $build_address = function ($type, $data, $custom_fields, $addresses) { + $new_id = empty($addresses) ? 1 : max(array_column($addresses, 'id')) + 1; + $addr = [ + 'id' => $new_id, + 'label' => ucfirst($type) . ' ' . $new_id, + 'type' => $type, + 'is_default' => empty($addresses), // default if it's the first one + ]; + + $standard_fields = ['first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'email', 'phone']; + foreach ($standard_fields as $f) { + if (isset($data[$f])) { + $addr[$f] = sanitize_text_field($data[$f]); + } + } + + // Add custom fields matching prefix + if (is_array($custom_fields)) { + foreach ($custom_fields as $k => $v) { + if (strpos($k, $type . '_') === 0) { + $addr[$k] = sanitize_text_field($v); + } elseif (!isset($addr[$type . '_' . $k]) && !isset($addr[$k])) { + // Some custom fields might not have the prefix if they apply to both + // Or they are sent without prefix by frontend in payload[type] + } + } + } + + // Also, payload[type] can contain custom fields directly because frontend sends them without prefix! + foreach ($data as $k => $v) { + if (!in_array($k, $standard_fields) && !in_array($k, ['ship_to_different'])) { + $addr[$type . '_' . $k] = sanitize_text_field($v); + } + } + return $addr; + }; + + $changed = false; + + // Check billing + if (!empty($payload['billing'])) { + if (!$is_duplicate($payload['billing'], 'billing')) { + $billing_addr = $build_address('billing', $payload['billing'], $payload['custom_fields'] ?? [], $addresses); + $addresses[] = $billing_addr; + $changed = true; + } + } + + // Check shipping + $ship_to_different = !empty($payload['shipping']['ship_to_different']); + if ($ship_to_different && !empty($payload['shipping'])) { + if (!$is_duplicate($payload['shipping'], 'shipping')) { + $shipping_addr = $build_address('shipping', $payload['shipping'], $payload['custom_fields'] ?? [], $addresses); + $addresses[] = $shipping_addr; + $changed = true; + } + } + + if ($changed) { + update_user_meta($user_id, 'woonoow_addresses', array_values($addresses)); + } + } } diff --git a/includes/Frontend/AddressController.php b/includes/Frontend/AddressController.php index ff48821..d92944e 100644 --- a/includes/Frontend/AddressController.php +++ b/includes/Frontend/AddressController.php @@ -69,6 +69,10 @@ class AddressController { $addresses = array_values($addresses); + foreach ($addresses as &$address) { + $address['formatted_address'] = apply_filters('woonoow_format_address', '', $address); + } + return new WP_REST_Response($addresses, 200); } diff --git a/snippets/rajaongkir-x-woonoow.php b/snippets/rajaongkir-x-woonoow.php index 3f6b5e3..d00ad4f 100644 --- a/snippets/rajaongkir-x-woonoow.php +++ b/snippets/rajaongkir-x-woonoow.php @@ -91,6 +91,11 @@ add_filter('woocommerce_checkout_fields', function ($fields) { $fields['billing']['billing_last_name']['type'] = 'hidden'; $fields['billing']['billing_last_name']['default'] = 'ID'; $fields['billing']['billing_last_name']['required'] = false; + + // Make first_name take full width since last_name is hidden + if (isset($fields['billing']['billing_first_name'])) { + $fields['billing']['billing_first_name']['class'] = ['form-row-wide']; + } } if (isset($fields['billing']['billing_country'])) { $fields['billing']['billing_country']['type'] = 'hidden'; @@ -115,6 +120,11 @@ add_filter('woocommerce_checkout_fields', function ($fields) { $fields['shipping']['shipping_last_name']['type'] = 'hidden'; $fields['shipping']['shipping_last_name']['default'] = 'ID'; $fields['shipping']['shipping_last_name']['required'] = false; + + // Make first_name take full width since last_name is hidden + if (isset($fields['shipping']['shipping_first_name'])) { + $fields['shipping']['shipping_first_name']['class'] = ['form-row-wide']; + } } if (isset($fields['shipping']['shipping_country'])) { $fields['shipping']['shipping_country']['type'] = 'hidden'; @@ -137,7 +147,8 @@ add_filter('woocommerce_checkout_fields', function ($fields) { // Check if cart needs shipping $needs_shipping = true; - if (function_exists('WC') && WC()->cart) { + // If cart is empty, we assume it's for Address Book in My Account where we want fields visible + if (function_exists('WC') && WC()->cart && !WC()->cart->is_empty()) { $needs_shipping = WC()->cart->needs_shipping(); } @@ -225,3 +236,79 @@ add_action('woonoow/shipping/before_calculate', function ($shipping, $items) { // Clear shipping cache to force recalculation WC()->session->set('shipping_for_package_0', false); }, 10, 2); + +// ============================================================ +// 4. Save destination_id to Order Meta on SPA Checkout +// ============================================================ +add_action('woocommerce_checkout_order_processed', function ($order_id, $payload) { + // Extract and save destination_id from shipping payload + if (!empty($payload['shipping']['destination_id'])) { + update_post_meta($order_id, '_shipping_destination_id', sanitize_text_field($payload['shipping']['destination_id'])); + } + + // Extract and save destination_id from billing payload + if (!empty($payload['billing']['destination_id'])) { + update_post_meta($order_id, '_billing_destination_id', sanitize_text_field($payload['billing']['destination_id'])); + } + + // Fallback to custom_fields array if present + if (!empty($payload['custom_fields']['shipping_destination_id'])) { + update_post_meta($order_id, '_shipping_destination_id', sanitize_text_field($payload['custom_fields']['shipping_destination_id'])); + } + if (!empty($payload['custom_fields']['billing_destination_id'])) { + update_post_meta($order_id, '_billing_destination_id', sanitize_text_field($payload['custom_fields']['billing_destination_id'])); + } + + // Save labels too if they exist, useful for backend viewing + if (!empty($payload['custom_fields']['shipping_destination_id_label'])) { + update_post_meta($order_id, '_shipping_destination_id_label', sanitize_text_field($payload['custom_fields']['shipping_destination_id_label'])); + } + if (!empty($payload['custom_fields']['billing_destination_id_label'])) { + update_post_meta($order_id, '_billing_destination_id_label', sanitize_text_field($payload['custom_fields']['billing_destination_id_label'])); + } +}, 10, 2); + +// ============================================================ +// 5. Format address display for SPA saved addresses +// ============================================================ +add_filter('woonoow_format_address', function ($formatted, $address) { + // If a snippet has already formatted it, skip + if (!empty($formatted)) { + return $formatted; + } + + $type = $address['type'] ?? 'billing'; + $is_billing = $type === 'billing' || $type === 'both'; + + // Look for destination_id_label + $label = ''; + if (!empty($address['destination_id_label'])) { + $label = $address['destination_id_label']; + } elseif ($is_billing && !empty($address['billing_destination_id_label'])) { + $label = $address['billing_destination_id_label']; + } elseif (!$is_billing && !empty($address['shipping_destination_id_label'])) { + $label = $address['shipping_destination_id_label']; + } + + // If we have a Rajaongkir label, construct a clean Indonesian address + if ($label) { + $parts = []; + if (!empty($address['address_1'])) { + $parts[] = $address['address_1']; + } + if (!empty($address['address_2'])) { + $parts[] = $address['address_2']; + } + // Append the Rajaongkir province/city/subdistrict string + $parts[] = $label; + + if (!empty($address['postcode'])) { + $parts[] = $address['postcode']; + } + + // The SPA uses whitespace-pre-wrap so newline \n works for visual separation + return implode("\n", $parts); + } + + return $formatted; +}, 10, 2);