feat(checkout): dynamic checkout fields with PHP filter support
Backend (CheckoutController): - Enhanced get_fields() API with custom_attributes, search_endpoint, search_param, min_chars, input_class, default - Supports new 'searchable_select' field type for API-backed search Customer SPA: - Created DynamicCheckoutField component for all field types - Checkout fetches fields from /checkout/fields API - Renders custom fields from PHP filters (billing + shipping) - searchable_select type with live API search - Custom field data included in checkout submission This enables: - Checkout Field Editor Pro compatibility - Rajaongkir destination_id via simple code snippet - Any plugin using woocommerce_checkout_fields filter Updated RAJAONGKIR_INTEGRATION.md with code snippet approach.
This commit is contained in:
@@ -4,6 +4,7 @@ import { useCartStore } from '@/lib/cart/store';
|
||||
import { useCheckoutSettings } from '@/hooks/useAppearanceSettings';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SearchableSelect } from '@/components/ui/searchable-select';
|
||||
import { DynamicCheckoutField, type CheckoutField } from '@/components/DynamicCheckoutField';
|
||||
import Container from '@/components/Layout/Container';
|
||||
import SEOHead from '@/components/SEOHead';
|
||||
import { formatPrice } from '@/lib/currency';
|
||||
@@ -154,6 +155,68 @@ export default function Checkout() {
|
||||
}
|
||||
}, [shippingData.country, states]);
|
||||
|
||||
// Dynamic checkout fields from API
|
||||
const [checkoutFields, setCheckoutFields] = useState<CheckoutField[]>([]);
|
||||
const [customFieldData, setCustomFieldData] = useState<Record<string, string>>({});
|
||||
|
||||
// Fetch checkout fields from API
|
||||
useEffect(() => {
|
||||
const loadCheckoutFields = async () => {
|
||||
if (cart.items.length === 0) return;
|
||||
|
||||
try {
|
||||
const items = cart.items.map(item => ({
|
||||
product_id: item.product_id,
|
||||
qty: item.quantity,
|
||||
}));
|
||||
|
||||
const data = await api.post<{
|
||||
ok: boolean;
|
||||
fields: CheckoutField[];
|
||||
is_digital_only: boolean;
|
||||
}>('/checkout/fields', { items, is_digital_only: isVirtualOnly });
|
||||
|
||||
if (data.ok && data.fields) {
|
||||
setCheckoutFields(data.fields);
|
||||
|
||||
// Initialize custom field values with defaults
|
||||
const customDefaults: Record<string, string> = {};
|
||||
data.fields.forEach(field => {
|
||||
if (field.custom && field.default) {
|
||||
customDefaults[field.key] = field.default;
|
||||
}
|
||||
});
|
||||
if (Object.keys(customDefaults).length > 0) {
|
||||
setCustomFieldData(prev => ({ ...customDefaults, ...prev }));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load checkout fields:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadCheckoutFields();
|
||||
}, [cart.items, isVirtualOnly]);
|
||||
|
||||
// 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);
|
||||
|
||||
// Handler for custom field changes
|
||||
const handleCustomFieldChange = (key: string, value: string) => {
|
||||
setCustomFieldData(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
// Listen for label events from searchable_select
|
||||
useEffect(() => {
|
||||
const handleLabelEvent = (e: Event) => {
|
||||
const { key, value } = (e as CustomEvent).detail;
|
||||
setCustomFieldData(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
document.addEventListener('woonoow:field_label', handleLabelEvent);
|
||||
return () => document.removeEventListener('woonoow:field_label', handleLabelEvent);
|
||||
}, []);
|
||||
|
||||
// Load saved addresses
|
||||
useEffect(() => {
|
||||
const loadAddresses = async () => {
|
||||
@@ -335,6 +398,10 @@ export default function Checkout() {
|
||||
state: billingData.state,
|
||||
postcode: billingData.postcode,
|
||||
country: billingData.country,
|
||||
// Include custom billing fields
|
||||
...Object.fromEntries(
|
||||
billingCustomFields.map(f => [f.key.replace('billing_', ''), customFieldData[f.key] || ''])
|
||||
),
|
||||
},
|
||||
shipping: shipToDifferentAddress ? {
|
||||
first_name: shippingData.firstName,
|
||||
@@ -345,11 +412,17 @@ export default function Checkout() {
|
||||
postcode: shippingData.postcode,
|
||||
country: shippingData.country,
|
||||
ship_to_different: true,
|
||||
// Include custom shipping fields
|
||||
...Object.fromEntries(
|
||||
shippingCustomFields.map(f => [f.key.replace('shipping_', ''), customFieldData[f.key] || ''])
|
||||
),
|
||||
} : {
|
||||
ship_to_different: false,
|
||||
},
|
||||
payment_method: paymentMethod,
|
||||
customer_note: orderNotes,
|
||||
// Include all custom field data for backend processing
|
||||
custom_fields: customFieldData,
|
||||
};
|
||||
|
||||
// Submit order
|
||||
@@ -578,6 +651,18 @@ export default function Checkout() {
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Custom billing fields from plugins */}
|
||||
{billingCustomFields.map(field => (
|
||||
<DynamicCheckoutField
|
||||
key={field.key}
|
||||
field={field}
|
||||
value={customFieldData[field.key] || ''}
|
||||
onChange={(v) => handleCustomFieldChange(field.key, v)}
|
||||
countryOptions={countryOptions}
|
||||
stateOptions={billingStateOptions}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -739,6 +824,18 @@ export default function Checkout() {
|
||||
className="w-full border rounded-lg px-4 py-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Custom shipping fields from plugins */}
|
||||
{shippingCustomFields.map(field => (
|
||||
<DynamicCheckoutField
|
||||
key={field.key}
|
||||
field={field}
|
||||
value={customFieldData[field.key] || ''}
|
||||
onChange={(v) => handleCustomFieldChange(field.key, v)}
|
||||
countryOptions={countryOptions}
|
||||
stateOptions={shippingStateOptions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user