Fix RajaOngkir address integration bugs and styling issues
This commit is contained in:
@@ -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 */}
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
<div className="text-sm text-gray-600 mt-2">
|
||||
{address.formatted_address ? (
|
||||
<p className="whitespace-pre-wrap">{address.formatted_address}</p>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
{address.address_1}
|
||||
{address.address_2 && `, ${address.address_2}`}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
<p>
|
||||
{address.city}, {address.state} {address.postcode}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{address.country}</p>
|
||||
<p>{address.country}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -345,10 +345,18 @@ export default function Addresses() {
|
||||
<div className="text-sm text-gray-700 space-y-1 mb-4">
|
||||
<p className="font-medium">{address.first_name} {address.last_name}</p>
|
||||
{address.company && <p>{address.company}</p>}
|
||||
{address.formatted_address ? (
|
||||
<p className="whitespace-pre-wrap">{address.formatted_address}</p>
|
||||
) : (
|
||||
<>
|
||||
<p>{address.address_1}</p>
|
||||
{address.address_2 && <p>{address.address_2}</p>}
|
||||
<p>{address.city}, {address.state} {address.postcode}</p>
|
||||
{[address.city, address.state, address.postcode].filter(Boolean).length > 0 && (
|
||||
<p>{[address.city, address.state, address.postcode].filter(Boolean).join(', ')}</p>
|
||||
)}
|
||||
<p>{address.country}</p>
|
||||
</>
|
||||
)}
|
||||
{address.phone && <p className="pt-2">Phone: {address.phone}</p>}
|
||||
{address.email && <p>Email: {address.email}</p>}
|
||||
</div>
|
||||
@@ -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}
|
||||
|
||||
@@ -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() {
|
||||
<div className="bg-primary/5 border-2 border-primary rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold">{sel.label}{sel.is_default && <span className="ml-2 text-xs bg-green-100 text-green-700 px-1.5 rounded">Default</span>}</p>
|
||||
<p>{sel.first_name} {sel.last_name}</p>
|
||||
<p className="text-gray-600">{sel.address_1}, {sel.city}, {sel.state} {sel.postcode}</p>
|
||||
{sel.formatted_address ? (
|
||||
<p className="text-gray-600 whitespace-pre-wrap">{sel.formatted_address}</p>
|
||||
) : (
|
||||
<div className="text-gray-600">
|
||||
<p>{sel.address_1}</p>
|
||||
{[sel.city, sel.state, sel.postcode].filter(Boolean).length > 0 && (
|
||||
<p>{[sel.city, sel.state, sel.postcode].filter(Boolean).join(', ')}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
@@ -866,7 +882,7 @@ export default function Checkout() {
|
||||
</div>
|
||||
)}
|
||||
{billingCustomFields.map(field => (
|
||||
<DynamicCheckoutField key={field.key} field={field} value={customFieldData[field.key] || ''} onChange={v => handleCustomFieldChange(field.key, v)} countryOptions={countryOptions} stateOptions={billingStateOptions} />
|
||||
<DynamicCheckoutField key={field.key} field={field} value={customFieldData[field.key] || ''} valueLabel={customFieldData[`${field.key}_label`]} onChange={v => handleCustomFieldChange(field.key, v)} countryOptions={countryOptions} stateOptions={billingStateOptions} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
@@ -908,7 +924,16 @@ export default function Checkout() {
|
||||
<div className="bg-primary/5 border-2 border-primary rounded-lg p-4 text-sm">
|
||||
<p className="font-semibold">{sel.label}</p>
|
||||
<p>{sel.first_name} {sel.last_name}</p>
|
||||
<p className="text-gray-600">{sel.address_1}, {sel.city} {sel.postcode}</p>
|
||||
{sel.formatted_address ? (
|
||||
<p className="text-gray-600 whitespace-pre-wrap">{sel.formatted_address}</p>
|
||||
) : (
|
||||
<div className="text-gray-600">
|
||||
<p>{sel.address_1}</p>
|
||||
{[sel.city, sel.state, sel.postcode].filter(Boolean).length > 0 && (
|
||||
<p>{[sel.city, sel.state, sel.postcode].filter(Boolean).join(', ')}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
@@ -926,7 +951,7 @@ export default function Checkout() {
|
||||
{getShippingField('shipping_country') && <div><label className="block text-sm font-medium mb-1">Country</label><SearchableSelect options={countryOptions} value={shippingData.country} onChange={v => setShippingData({ ...shippingData, country: v })} placeholder="Select country" disabled={countries.length === 1} /></div>}
|
||||
{getShippingField('shipping_state') && <div><label className="block text-sm font-medium mb-1">State</label>{shippingStateOptions.length > 0 ? <SearchableSelect options={shippingStateOptions} value={shippingData.state} onChange={v => setShippingData({ ...shippingData, state: v })} placeholder="Select state" /> : <input type="text" value={shippingData.state} onChange={e => setShippingData({ ...shippingData, state: e.target.value })} className="w-full border rounded-lg px-4 py-2" />}</div>}
|
||||
{getShippingField('shipping_postcode') && <div><label className="block text-sm font-medium mb-1">Postcode</label><input type="text" value={shippingData.postcode} onChange={e => setShippingData({ ...shippingData, postcode: e.target.value })} className="w-full border rounded-lg px-4 py-2" /></div>}
|
||||
{shippingCustomFields.map(field => <DynamicCheckoutField key={field.key} field={field} value={customFieldData[field.key] || ''} onChange={v => handleCustomFieldChange(field.key, v)} countryOptions={countryOptions} stateOptions={shippingStateOptions} />)}
|
||||
{shippingCustomFields.map(field => <DynamicCheckoutField key={field.key} field={field} value={customFieldData[field.key] || ''} valueLabel={customFieldData[`${field.key}_label`]} onChange={v => handleCustomFieldChange(field.key, v)} countryOptions={countryOptions} stateOptions={shippingStateOptions} />)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user