Files
WooNooW/docs/SHIPPING_GUIDE.md

7.0 KiB

WooNooW Shipping & Integrations Guide

Version: 2.0.0
Status: Production Ready

This document outlines how shipping methods work within WooNooW, and how to bridge complex external/API shipping plugins (such as RajaOngkir or Sicepat) fully into the Customer SPA Order Form.


📋 Table of Contents

  1. How WooNooW Handles Shipping
  2. The Shipping Bridge Pattern
  3. RajaOngkir Integration Example
  4. Custom Shipping Addons

1. How WooNooW Handles Shipping

Two Types of Shipping Methods

WooCommerce Core inherently provides two types of shipping:

1. Static Methods (e.g. Free Shipping, Flat Rate)

  • Immediate calculation (no API).
  • Configured via basic Address fields (Country, State, City, ZIP).

2. Live Rate Methods (API Based)

  • Need an API Call (e.g., UPS, FedEx, or local Indonesian couriers like JNE/J&T).
  • May require extremely specific address fragments to calculate rates accurately.
    • International APIs (UPS/FedEx): Usually rely on Postal Code.
    • Indonesian APIs (RajaOngkir/Biteship): Usually rely on Subdistrict IDs.

The Problem

If a customer uses an Indonesian shipping plugin, the plugin may remove default WooCommerce fields (like city/state) and inject a custom dropdown that searches an API for a destination. The plugin then writes that Destination ID to the WooCommerce Session and forces shipping recalculation.

Because WooNooW's SPA bypasses normal PHP frontend rendering, native injected dropdowns from external plugins will not appear automatically.


2. The Shipping Bridge Pattern

To make external shipping plugins work natively within WooNooW's SPA checkout, use our Bridge Hook System.

You need 3 components:

  1. REST API Endpoint: To let the SPA search the provider's valid locations.
  2. Checkout Field Injection: To inject a searchable_select field into the SPA order form natively.
  3. Bridge Action Hook: To take the selected value from the order form and write it to the WooCommerce Session before shipping calculates.

Generic Bridge Skeleton

// 1. Endpoint for SPA to search locations
add_action('rest_api_init', function() {
    register_rest_route('woonoow/v1', '/provider/search', [
        'methods' => 'GET',
        'callback' => 'my_provider_search_func',
        'permission_callback' => '__return_true',
    ]);
});

// 2. Inject field into WooNooW SPA (native JSON structure, not HTML)
add_filter('woocommerce_checkout_fields', function($fields) {
    if (!class_exists('My_Shipping_Class')) return $fields;
    
    $fields['shipping']['shipping_provider_field'] = [
        'type'            => 'searchable_select',
        'label'           => 'Select Location',
        'required'        => true,
        'priority'        => 85,
        'search_endpoint' => '/provider/search', // Relative to wp-json/woonoow/v1
        'search_param'    => 'search',
        'min_chars'       => 3,
    ];
    return $fields;
}, 20);

// 3. Bridge Data to Provider Session
add_action('woonoow/shipping/before_calculate', function($shipping, $items) {
    if (!class_exists('My_Shipping_Class')) return;
    
    $val = $shipping['shipping_provider_field'] ?? null;
    if ($val) {
        WC()->session->set('my_provider_session_key', $val);
        // Force recalc
        WC()->session->set('shipping_for_package_0', false); 
    }
}, 10, 2);

3. RajaOngkir Integration Example

If your user wants RajaOngkir running natively inside the WooNooW SPA checkout, this snippet represents the "Best Practice Bridge".

Note: Drop this into a Code Snippets plugin or functions.php

<?php
// 1. Search Endpoint
add_action('rest_api_init', function() {
    register_rest_route('woonoow/v1', '/rajaongkir/destinations', [
        'methods'  => 'GET',
        'callback' => function($req) {
            $search = sanitize_text_field($req->get_param('search') ?? '');
            if (strlen($search) < 3 || !class_exists('Cekongkir_API')) return [];
            
            $api = Cekongkir_API::get_instance();
            $results = $api->search_destination_api($search);
            
            $formatted = [];
            if (is_array($results)) {
                foreach ($results as $r) {
                    $formatted[] = [
                        'value' => (string) ($r['id'] ?? ''),
                        'label' => $r['label'] ?? $r['text'] ?? '',
                    ];
                }
            }
            return array_slice($formatted, 0, 50);
        },
        'permission_callback' => '__return_true'
    ]);
});

// 2. Register Native SPA Field & cleanup unnecessary native fields
add_filter('woocommerce_checkout_fields', function($fields) {
    if (!class_exists('Cekongkir_API')) return $fields;

    // Check if store only ships to Indonesia to safely hide fallback fields
    $allowed = WC()->countries->get_allowed_countries();
    $indonesia_only = (count($allowed) === 1 && isset($allowed['ID']));
    
    if ($indonesia_only) {
        $fields['shipping']['shipping_country']['type'] = 'hidden';
        $fields['shipping']['shipping_state']['type'] = 'hidden';
        $fields['shipping']['shipping_city']['type'] = 'hidden';
        $fields['shipping']['shipping_postcode']['type'] = 'hidden';
    }

    $dest_field = [
        'type'            => 'searchable_select',
        'label'           => __('Destination (Province, City, Subdistrict)', 'woonoow'),
        'required'        => $indonesia_only, 
        'priority'        => 85,
        'search_endpoint' => '/rajaongkir/destinations',
        'search_param'    => 'search',
        'min_chars'       => 3,
        'custom_attributes' => ['data-show-for-country' => 'ID'],
    ];
    
    $fields['billing']['billing_destination_id'] = $dest_field;
    $fields['shipping']['shipping_destination_id'] = $dest_field;
    return $fields;
}, 20);

// 3. Bridge Selection back into WooCommerce specific session key
add_action('woonoow/shipping/before_calculate', function($shipping, $items) {
    if (!class_exists('Cekongkir_API')) return;

    $country = $shipping['country'] ?? WC()->customer->get_shipping_country();
    if ($country !== 'ID') return;
    
    $dest_id = $shipping['destination_id'] 
             ?? $shipping['shipping_destination_id']
             ?? $shipping['billing_destination_id'] ?? null;
             
    if ($dest_id) {
        WC()->session->set('selected_destination_id', intval($dest_id));
        WC()->session->set('shipping_for_package_0', false);
    }
}, 10, 2);

4. Custom Shipping Addons

If you do not want to use an external plugin and instead wish to build an official WooNooW extension (like WooNooW Indonesia Shipping with Biteship capabilities), treat it as a standard WooNooW Addon Module.

  1. Develop a native React UI component using ADDONS_GUIDE.md.
  2. Intercept woonoow_order_form_after_shipping via Hook.
  3. Validate and handle the custom data payload via the standard WooCommerce REST endpoints.