# Rajaongkir Integration with WooNooW This guide explains how to bridge the Rajaongkir shipping plugin with WooNooW's admin order form and checkout flow. --- ## The Challenge Rajaongkir doesn't use standard WooCommerce address fields. Instead of using `city` and `state`, it requires a **destination ID** from its own Indonesian location database. **Standard WooCommerce Flow:** ``` Country → State → City → Postcode ``` **Rajaongkir Flow:** ``` Country (ID) → Destination ID (subdistrict level) ``` --- ## WooNooW Integration Hook WooNooW provides a hook that fires **before** shipping calculation. This allows plugins like Rajaongkir to set session variables or prepare any data they need. ### Hook: `woonoow/shipping/before_calculate` ```php do_action( 'woonoow/shipping/before_calculate', $shipping_data, $items ); ``` **Parameters:** - `$shipping_data` (array) - The shipping address from frontend: - `country` - Country code (e.g., 'ID') - `state` - State code - `city` - City name - `postcode` - Postal code - `address_1` - Street address - `destination_id` - Custom field for Rajaongkir (added via addon) - + Any custom fields added by addons - `$items` (array) - Cart items being shipped --- ## Complete Integration: Searchable Destination Selector This is a complete code solution that adds a searchable Rajaongkir destination selector to WooNooW checkout. ### Plugin File: `woonoow-rajaongkir-bridge.php` Create this as a new plugin or add to your theme's functions.php: ```php session->__unset( 'selected_destination_id' ); WC()->session->__unset( 'selected_destination_label' ); return; } // Set destination from frontend if ( ! empty( $shipping['destination_id'] ) ) { WC()->session->set( 'selected_destination_id', intval( $shipping['destination_id'] ) ); WC()->session->set( 'selected_destination_label', sanitize_text_field( $shipping['destination_label'] ?? '' ) ); } }, 10, 2 ); /** * 2. REST API: Search Rajaongkir destinations */ add_action( 'rest_api_init', function() { register_rest_route( 'woonoow/v1', '/rajaongkir/destinations', [ 'methods' => 'GET', 'callback' => 'woonoow_search_rajaongkir_destinations', 'permission_callback' => '__return_true', 'args' => [ 'search' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], ], ]); }); function woonoow_search_rajaongkir_destinations( $request ) { $search = $request->get_param( 'search' ); if ( strlen( $search ) < 2 ) { return new WP_REST_Response( [], 200 ); } // Get Rajaongkir destinations from transient/cache $destinations = get_transient( 'cekongkir_all_destinations' ); if ( ! $destinations ) { // Fetch from Rajaongkir API or database $destinations = woonoow_fetch_rajaongkir_destinations(); set_transient( 'cekongkir_all_destinations', $destinations, DAY_IN_SECONDS ); } // Filter by search term $results = array_filter( $destinations, function( $dest ) use ( $search ) { return stripos( $dest['label'], $search ) !== false; }); // Limit to 50 results $results = array_slice( array_values( $results ), 0, 50 ); return new WP_REST_Response( $results, 200 ); } /** * 3. Fetch destinations from Rajaongkir database */ function woonoow_fetch_rajaongkir_destinations() { global $wpdb; $destinations = []; // Try to get from Rajaongkir plugin's stored data $table = $wpdb->prefix . 'cekongkir_destinations'; if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) === $table ) { $rows = $wpdb->get_results( "SELECT * FROM $table", ARRAY_A ); foreach ( $rows as $row ) { $destinations[] = [ 'value' => $row['id'], 'label' => $row['province'] . ', ' . $row['city'] . ', ' . $row['subdistrict'], 'province' => $row['province'], 'city' => $row['city'], 'subdistrict' => $row['subdistrict'], ]; } } // Fallback: Use options storage if ( empty( $destinations ) ) { $stored = get_option( 'cekongkir_destinations', [] ); foreach ( $stored as $id => $label ) { $destinations[] = [ 'value' => $id, 'label' => $label, ]; } } return $destinations; } /** * 4. Enqueue scripts for checkout */ add_action( 'wp_enqueue_scripts', function() { if ( ! is_checkout() && ! is_wc_endpoint_url() ) { return; } wp_enqueue_script( 'woonoow-rajaongkir-bridge', plugin_dir_url( __FILE__ ) . 'rajaongkir-bridge.js', [], '1.0.0', true ); wp_localize_script( 'woonoow-rajaongkir-bridge', 'WNW_RAJAONGKIR', [ 'apiUrl' => rest_url( 'woonoow/v1/rajaongkir/destinations' ), 'nonce' => wp_create_nonce( 'wp_rest' ), ]); }); ``` ### JavaScript: `rajaongkir-bridge.js` This script adds a searchable destination selector to the checkout: ```javascript /** * Rajaongkir Destination Selector for WooNooW * * Adds a searchable dropdown to select Indonesian subdistrict */ (function() { 'use strict'; // Only run on checkout if (!document.querySelector('.woonoow-checkout, .woocommerce-checkout')) { return; } // Wait for DOM document.addEventListener('DOMContentLoaded', function() { initRajaongkirSelector(); }); function initRajaongkirSelector() { // Find the city/state fields in shipping form const countryField = document.querySelector('[name="shipping_country"], [name="billing_country"]'); if (!countryField) return; // Check if Indonesia is selected const checkCountry = () => { const country = countryField.value; if (country === 'ID') { showDestinationSelector(); } else { hideDestinationSelector(); } }; countryField.addEventListener('change', checkCountry); checkCountry(); } function showDestinationSelector() { // Create searchable destination field if not exists if (document.querySelector('#rajaongkir-destination-wrapper')) { document.querySelector('#rajaongkir-destination-wrapper').style.display = 'block'; return; } const wrapper = document.createElement('div'); wrapper.id = 'rajaongkir-destination-wrapper'; wrapper.className = 'form-group md:col-span-2'; wrapper.innerHTML = `
`; // Insert after postcode field const postcodeField = document.querySelector('[name="shipping_postcode"], [name="billing_postcode"]'); if (postcodeField) { postcodeField.closest('.form-group, div')?.after(wrapper); } setupSearchHandler(); } function hideDestinationSelector() { const wrapper = document.querySelector('#rajaongkir-destination-wrapper'); if (wrapper) { wrapper.style.display = 'none'; } } let searchTimeout; function setupSearchHandler() { const searchInput = document.querySelector('#rajaongkir-search'); const resultsDiv = document.querySelector('#rajaongkir-results'); const destinationIdInput = document.querySelector('#rajaongkir-destination-id'); const destinationLabelInput = document.querySelector('#rajaongkir-destination-label'); searchInput.addEventListener('input', function(e) { const query = e.target.value; clearTimeout(searchTimeout); if (query.length < 2) { resultsDiv.classList.add('hidden'); return; } searchTimeout = setTimeout(() => { searchDestinations(query); }, 300); }); async function searchDestinations(query) { try { const response = await fetch( `${WNW_RAJAONGKIR.apiUrl}?search=${encodeURIComponent(query)}`, { headers: { 'X-WP-Nonce': WNW_RAJAONGKIR.nonce, }, } ); const destinations = await response.json(); if (destinations.length === 0) { resultsDiv.innerHTML = '
No destinations found
'; } else { resultsDiv.innerHTML = destinations.map(dest => `
${dest.label}
`).join(''); } resultsDiv.classList.remove('hidden'); // Setup click handlers resultsDiv.querySelectorAll('[data-value]').forEach(item => { item.addEventListener('click', function() { const value = this.dataset.value; const label = this.dataset.label; searchInput.value = label; destinationIdInput.value = value; destinationLabelInput.value = label; resultsDiv.classList.add('hidden'); // Trigger shipping calculation document.body.dispatchEvent(new Event('woonoow:address_changed')); }); }); } catch (error) { console.error('Rajaongkir search error:', error); } } // Hide on click outside document.addEventListener('click', function(e) { if (!e.target.closest('#rajaongkir-destination-wrapper')) { resultsDiv.classList.add('hidden'); } }); } })(); ``` --- ## React Component Version (for customer-spa) If you're extending the customer-spa directly, here's a React component: ```tsx // components/RajaongkirDestinationSelector.tsx import React, { useState, useEffect } from 'react'; import { api } from '@/lib/api/client'; import { SearchableSelect } from '@/components/ui/searchable-select'; interface DestinationOption { value: string; label: string; } interface Props { value?: string; label?: string; onChange: (id: string, label: string) => void; } export function RajaongkirDestinationSelector({ value, label, onChange }: Props) { const [search, setSearch] = useState(''); const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { if (search.length < 2) { setOptions([]); return; } const timer = setTimeout(async () => { setLoading(true); try { const data = await api.get('/rajaongkir/destinations', { search }); setOptions(data); } catch (error) { console.error('Failed to search destinations:', error); } finally { setLoading(false); } }, 300); return () => clearTimeout(timer); }, [search]); const handleSelect = (selectedValue: string) => { const selected = options.find(o => o.value === selectedValue); if (selected) { onChange(selected.value, selected.label); } }; return (
{label && (

Selected: {label}

)}
); } ``` Usage in Checkout: ```tsx // In Checkout/index.tsx import { RajaongkirDestinationSelector } from '@/components/RajaongkirDestinationSelector'; // In the shipping form, add: {shippingData.country === 'ID' && (
setShippingData({ ...shippingData, destination_id: id, destination_label: label, })} />
)} ``` --- ## Testing 1. Install Rajaongkir plugin and configure API key 2. Add the bridge code above 3. Go to WooNooW Checkout 4. Set country to Indonesia 5. Type in the destination search field 6. Select a destination from dropdown 7. Click "Calculate Shipping" 8. Verify Rajaongkir rates appear --- ## Troubleshooting ### Destinations not loading? Check the REST API endpoint: ``` GET /wp-json/woonoow/v1/rajaongkir/destinations?search=jakarta ``` ### Session not persisting? Verify the hook is firing: ```php add_action( 'woonoow/shipping/before_calculate', function( $shipping, $items ) { error_log( 'Shipping data: ' . print_r( $shipping, true ) ); }, 5, 2 ); ``` --- ## Related Documentation - [SHIPPING_INTEGRATION.md](SHIPPING_INTEGRATION.md) - General shipping integration guide - [SHIPPING_METHOD_TYPES.md](SHIPPING_METHOD_TYPES.md) - Shipping method types explanation - [Rajaongkir Plugin Documentation](https://cekongkir.com/docs)