# WooNooW Metabox & Custom Fields Compatibility ## Philosophy: 3-Level Compatibility Strategy Following `ADDON_BRIDGE_PATTERN.md`, we support plugins at 3 levels: ### **Level 1: Native WP/WooCommerce Hooks** 🟢 (THIS DOCUMENT) **Community does NOTHING extra** - We listen automatically - Plugins use standard `add_meta_box()`, `update_post_meta()` - Store data in WooCommerce order/product meta - WooNooW exposes this data via API automatically - **Status: ❌ NOT IMPLEMENTED - MUST DO NOW** ### **Level 2: Bridge Snippets** 🟡 (See ADDON_BRIDGE_PATTERN.md) **Community creates simple bridge** - For non-standard behavior - Plugins that bypass standard hooks (e.g., Rajaongkir custom UI) - WooNooW provides hook system + documentation - Community creates bridge snippets - **Status: ✅ Hook system exists, documentation provided** ### **Level 3: Native WooNooW Addons** 🔵 (See ADDON_BRIDGE_PATTERN.md) **Community builds proper addons** - Best experience - Native WooNooW integration - Uses WooNooW addon system - Independent plugins - **Status: ✅ Addon system exists, developer docs provided** --- ## Current Status: ❌ LEVEL 1 NOT IMPLEMENTED **Critical Gap:** Our SPA admin does NOT currently expose custom meta fields from plugins that use standard WordPress/WooCommerce hooks. ### Example Use Case (Level 1): ```php // Plugin: WooCommerce Shipment Tracking // Uses STANDARD WooCommerce meta storage // Plugin stores data (standard WooCommerce way) update_post_meta($order_id, '_tracking_number', '1234567890'); update_post_meta($order_id, '_tracking_provider', 'JNE'); // Plugin displays in classic admin (standard metabox) add_meta_box('wc_shipment_tracking', 'Tracking Info', function($post) { $tracking = get_post_meta($post->ID, '_tracking_number', true); echo ''; }, 'shop_order'); ``` **Current WooNooW Behavior:** - ❌ API doesn't expose `_tracking_number` meta - ❌ Frontend can't read/write this data - ❌ Plugin's data exists in DB but not accessible **Expected WooNooW Behavior (Level 1):** - ✅ API exposes `meta` object with all fields - ✅ Frontend can read/write meta data - ✅ Plugin works WITHOUT any bridge/addon - ✅ **Community does NOTHING extra** --- ## Problem Analysis ### 1. Orders API (`OrdersController.php`) **Current Implementation:** ```php public static function show(WP_REST_Request $req) { $order = wc_get_order($id); $data = [ 'id' => $order->get_id(), 'status' => $order->get_status(), 'billing' => [...], 'shipping' => [...], 'items' => [...], // ... hardcoded fields only ]; return new WP_REST_Response($data, 200); } ``` **Missing:** - ❌ No `get_meta_data()` exposure - ❌ No `apply_filters('woonoow/order_data', $data, $order)` - ❌ No metabox hook listening - ❌ No custom field groups ### 2. Products API (`ProductsController.php`) **Current Implementation:** ```php public static function get_product(WP_REST_Request $request) { $product = wc_get_product($id); return new WP_REST_Response([ 'id' => $product->get_id(), 'name' => $product->get_name(), // ... hardcoded fields only ], 200); } ``` **Missing:** - ❌ No custom product meta exposure - ❌ No `apply_filters('woonoow/product_data', $data, $product)` - ❌ No ACF/CMB2/Pods integration - ❌ No custom tabs/panels --- ## Solution Architecture ### Phase 1: Meta Data Exposure (API Layer) #### 1.1 Orders API Enhancement **Add to `OrdersController::show()`:** ```php public static function show(WP_REST_Request $req) { $order = wc_get_order($id); // ... existing data ... // Expose all meta data $meta_data = []; foreach ($order->get_meta_data() as $meta) { $key = $meta->key; // Skip internal/private meta (starts with _) // unless explicitly allowed if (strpos($key, '_') === 0) { $allowed_private = apply_filters('woonoow/order_allowed_private_meta', [ '_tracking_number', '_tracking_provider', '_shipment_tracking_items', '_wc_shipment_tracking_items', // Add more as needed ], $order); if (!in_array($key, $allowed_private, true)) { continue; } } $meta_data[$key] = $meta->value; } $data['meta'] = $meta_data; // Allow plugins to add/modify data $data = apply_filters('woonoow/order_api_data', $data, $order, $req); return new WP_REST_Response($data, 200); } ``` **Add to `OrdersController::update()`:** ```php public static function update(WP_REST_Request $req) { $order = wc_get_order($id); $data = $req->get_json_params(); // ... existing update logic ... // Update custom meta fields if (isset($data['meta']) && is_array($data['meta'])) { foreach ($data['meta'] as $key => $value) { // Validate meta key is allowed $allowed = apply_filters('woonoow/order_updatable_meta', [ '_tracking_number', '_tracking_provider', // Add more as needed ], $order); if (in_array($key, $allowed, true)) { $order->update_meta_data($key, $value); } } } $order->save(); // Allow plugins to perform additional updates do_action('woonoow/order_updated', $order, $data, $req); return new WP_REST_Response(['success' => true], 200); } ``` #### 1.2 Products API Enhancement **Add to `ProductsController::get_product()`:** ```php public static function get_product(WP_REST_Request $request) { $product = wc_get_product($id); // ... existing data ... // Expose all meta data $meta_data = []; foreach ($product->get_meta_data() as $meta) { $key = $meta->key; // Skip internal meta unless allowed if (strpos($key, '_') === 0) { $allowed_private = apply_filters('woonoow/product_allowed_private_meta', [ '_custom_field_example', // Add more as needed ], $product); if (!in_array($key, $allowed_private, true)) { continue; } } $meta_data[$key] = $meta->value; } $data['meta'] = $meta_data; // Allow plugins to add/modify data $data = apply_filters('woonoow/product_api_data', $data, $product, $request); return new WP_REST_Response($data, 200); } ``` --- ### Phase 2: Frontend Rendering (React Components) #### 2.1 Dynamic Meta Fields Component **Create: `admin-spa/src/components/MetaFields.tsx`** ```tsx interface MetaField { key: string; label: string; type: 'text' | 'textarea' | 'number' | 'select' | 'date'; options?: Array<{value: string; label: string}>; section?: string; // Group fields into sections } interface MetaFieldsProps { meta: Record; fields: MetaField[]; onChange: (key: string, value: any) => void; readOnly?: boolean; } export function MetaFields({ meta, fields, onChange, readOnly }: MetaFieldsProps) { // Group fields by section const sections = fields.reduce((acc, field) => { const section = field.section || 'Other'; if (!acc[section]) acc[section] = []; acc[section].push(field); return acc; }, {} as Record); return (
{Object.entries(sections).map(([section, sectionFields]) => ( {section} {sectionFields.map(field => (
{field.type === 'text' && ( onChange(field.key, e.target.value)} disabled={readOnly} /> )} {field.type === 'textarea' && (