fix(checkout): fix disabled country/state and add public countries API

Issues fixed:
1. Country field was disabled when API failed (length 0)
   - Changed: disabled={countries.length <= 1} → disabled={countries.length === 1}
   - Only disables in single-country mode now

2. State field was disabled when no preloaded states
   - Changed: Falls back to text input instead of disabled SearchableSelect
   - Allows manual state entry for countries without state list

3. /countries API required admin permission
   - Added public /countries endpoint to CheckoutController
   - Uses permission_callback __return_true for customer checkout access
   - Returns countries, states, and default_country
This commit is contained in:
Dwindi Ramadhana
2026-01-08 14:02:13 +07:00
parent 6694d9e0c4
commit 274c3d35e1
2 changed files with 78 additions and 16 deletions

View File

@@ -628,18 +628,27 @@ export default function Checkout() {
value={billingData.country} value={billingData.country}
onChange={(v) => setBillingData({ ...billingData, country: v })} onChange={(v) => setBillingData({ ...billingData, country: v })}
placeholder="Select country" placeholder="Select country"
disabled={countries.length <= 1} disabled={countries.length === 1}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-2">State / Province *</label> <label className="block text-sm font-medium mb-2">State / Province *</label>
<SearchableSelect {billingStateOptions.length > 0 ? (
options={billingStateOptions} <SearchableSelect
value={billingData.state} options={billingStateOptions}
onChange={(v) => setBillingData({ ...billingData, state: v })} value={billingData.state}
placeholder={billingStateOptions.length ? "Select state" : "N/A"} onChange={(v) => setBillingData({ ...billingData, state: v })}
disabled={!billingStateOptions.length} placeholder="Select state"
/> />
) : (
<input
type="text"
value={billingData.state}
onChange={(e) => setBillingData({ ...billingData, state: e.target.value })}
placeholder="Enter state/province"
className="w-full border rounded-lg px-4 py-2"
/>
)}
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-2">Postcode / ZIP *</label> <label className="block text-sm font-medium mb-2">Postcode / ZIP *</label>
@@ -801,18 +810,27 @@ export default function Checkout() {
value={shippingData.country} value={shippingData.country}
onChange={(v) => setShippingData({ ...shippingData, country: v })} onChange={(v) => setShippingData({ ...shippingData, country: v })}
placeholder="Select country" placeholder="Select country"
disabled={countries.length <= 1} disabled={countries.length === 1}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-2">State / Province *</label> <label className="block text-sm font-medium mb-2">State / Province *</label>
<SearchableSelect {shippingStateOptions.length > 0 ? (
options={shippingStateOptions} <SearchableSelect
value={shippingData.state} options={shippingStateOptions}
onChange={(v) => setShippingData({ ...shippingData, state: v })} value={shippingData.state}
placeholder={shippingStateOptions.length ? "Select state" : "N/A"} onChange={(v) => setShippingData({ ...shippingData, state: v })}
disabled={!shippingStateOptions.length} placeholder="Select state"
/> />
) : (
<input
type="text"
value={shippingData.state}
onChange={(e) => setShippingData({ ...shippingData, state: e.target.value })}
placeholder="Enter state/province"
className="w-full border rounded-lg px-4 py-2"
/>
)}
</div> </div>
<div> <div>
<label className="block text-sm font-medium mb-2">Postcode / ZIP *</label> <label className="block text-sm font-medium mb-2">Postcode / ZIP *</label>

View File

@@ -32,6 +32,12 @@ class CheckoutController {
'callback' => [ new self(), 'get_fields' ], 'callback' => [ new self(), 'get_fields' ],
'permission_callback' => [ \WooNooW\Api\Permissions::class, 'anon_or_wp_nonce' ], 'permission_callback' => [ \WooNooW\Api\Permissions::class, 'anon_or_wp_nonce' ],
]); ]);
// Public countries endpoint for customer checkout form
register_rest_route($namespace, '/countries', [
'methods' => 'GET',
'callback' => [ new self(), 'get_countries' ],
'permission_callback' => '__return_true', // Public - needed for checkout
]);
// Public order view endpoint for thank you page // Public order view endpoint for thank you page
register_rest_route($namespace, '/checkout/order/(?P<id>\d+)', [ register_rest_route($namespace, '/checkout/order/(?P<id>\d+)', [
'methods' => 'GET', 'methods' => 'GET',
@@ -729,4 +735,42 @@ class CheckoutController {
} }
return null; return null;
} }
/**
* Get countries and states for checkout form
* Public endpoint - no authentication required
*/
public function get_countries(): array {
$wc_countries = WC()->countries;
// Get allowed selling countries
$allowed = $wc_countries->get_allowed_countries();
// Format for frontend
$countries = [];
foreach ($allowed as $code => $name) {
$countries[] = [
'code' => $code,
'name' => $name,
];
}
// Get states for all allowed countries
$states = [];
foreach (array_keys($allowed) as $country_code) {
$country_states = $wc_countries->get_states($country_code);
if (!empty($country_states) && is_array($country_states)) {
$states[$country_code] = $country_states;
}
}
// Get default country
$default_country = $wc_countries->get_base_country();
return [
'countries' => $countries,
'states' => $states,
'default_country' => $default_country,
];
}
} }