Kartu yang ditolak termasuk kegagalan yang sopan. Waktu checkout WooCommerce Blocks di toko yang saya bangun dites dengan kartu uji 4000 0000 0000 0002, pesan merah "Your card was declined." muncul inline di bawah nomor kartu, ditambah banner di bagian atas halaman. Order uji yang lolos juga gampang dibaca: halamannya langsung pindah ke order-received tanpa singgah di halaman Stripe, dan ordernya jadi Processing dalam 3 detik lewat webhook.
Yang menyusahkan adalah akhir ketiga. Place Order ditekan, tidak terjadi apa-apa, dan tidak ada satu request pun yang terkirim. Penyebabnya ternyata bukan kode. Alamat tersimpan lama di akun uji tidak punya state, dan error validasinya hanya ada di store data wc/store/validation. Satu bagian dari catatan saya soal penyebab itu juga tidak cocok dengan kode WooCommerce yang bisa saya baca, dan bagian itu saya biarkan terbuka di bawah.
Tanpa request, masalahnya masih di browser
Kalau tidak ada request yang keluar, Stripe dan webhook tidak sempat ikut campur. Pembayaran toko ini memakai plugin WooCommerce Stripe Gateway 10.9.1, tapi untuk bug ini plugin itu tidak relevan, karena memang tidak ada yang dikirim ke mana pun. Apa pun yang menahan tombolnya terjadi di browser, sebelum checkout memutuskan untuk mengirim sesuatu.
Bagian browser dari checkout Blocks adalah pohon React. Komentar di mu-plugin toko yang mengurus cart dan checkout merangkumnya dalam satu kalimat: kedua halaman itu WooCommerce Blocks, "so there is no PHP template to override and no hook inside the React tree". Bahkan label tombolnya tinggal di JavaScript. Di WooCommerce 11.1.0, tombol Place order, tombol Proceed, dan keempat blok langkah checkout tidak punya properti judul atau label sebagai atribut blok, dan itu dicek satu per satu lewat /wp-json/wp/v2/block-types/. Label-label itu string di dalam JavaScript WooCommerce, jadi toko ini menggantinya lewat wp.i18n.setLocaleData pada handle wp-i18n, hanya di halaman cart dan checkout.
Kalau teks tombolnya saja ada di JavaScript, error yang menahan tombol itu juga pantas dicari di sana. State checkout Blocks disimpan di store wp.data, dan skrip toko ini sendiri sudah membaca salah satunya, store keranjang, lewat window.wp.data.select('wc/store/cart').
Errornya ada, tapi hanya di store validasi
Untuk tombol yang diam ini, error validasinya hanya ada di store wc/store/validation. Dari luar, tombol bayarnya seperti mati.
Perintah untuk membaca store itu tidak saya salin dari catatan proyek. Saya menurunkannya dari bundel wc-blocks-data.js WooCommerce 10.6.2, yang menyimpan kunci store sebagai string "wc/store/validation", mendaftarkannya lewat createReduxStore lalu register milik window.wp.data, dan memberinya empat selector: getValidationError, getValidationErrorId, getValidationErrors, dan hasValidationErrors. Dengan pola yang sama seperti store keranjang tadi, bentuk minimalnya begini:
// Contoh minimal, diturunkan dari bundel WooCommerce 10.6.2.
// Jalankan di konsol browser pada halaman checkout.
const v = window.wp.data.select('wc/store/validation');
v.hasValidationErrors(); // true kalau ada satu error saja
v.getValidationErrors(); // seluruh isi store validasiIsi selector-nya menjelaskan kenapa dua panggilan itu cukup. Di bundel yang sama, getValidationErrors menunjuk ke fungsi ta, hasValidationErrors ke ea, dan getValidationErrorId ke Js:
// wc-blocks-data.js, WooCommerce 10.6.2, dikutip per selector dari kode minified
ta=e=>e
ea=e=>Object.keys(e).length>0
Js=(e,t)=>{if(e.hasOwnProperty(t)&&!e[t].hidden)return`validate-error-${t}`}getValidationErrors mengembalikan seluruh state store apa adanya. hasValidationErrors bernilai true selama state itu punya sedikitnya satu kunci, jadi satu error sudah cukup. Yang paling menarik buat kasus seperti ini adalah getValidationErrorId. Dia hanya mengembalikan id elemen error, berupa validate-error- diikuti kunci errornya, kalau error itu ada di state dan tidak bertanda hidden. Di kode ini, sebuah error bisa tersimpan di store tanpa mendapat id elemen.
Ada juga jalur untuk memunculkan error yang tersembunyi. Di checkout-frontend.js 10.6.2, satu komponen baru bekerja kalau tiga syarat benar bersamaan: checkout sedang idle, checkout punya error, dan store validasi berisi error. Kalau ketiganya terpenuhi, komponen itu memanggil showAllValidationErrors, lalu 50 ms kemudian menggulir ke elemen pertama yang cocok dengan input:invalid, .has-error input, .has-error select.
Apakah jalur itu yang tidak terpicu di toko, saya tidak tahu, dan tidak akan saya tebak. Kode yang bisa saya kutip berasal dari salinan WooCommerce 10.6.2 yang terpasang di folder proyek lain di disk saya, sementara komentar kode toko itu menyebut WooCommerce 11.1.0, dicek terhadap halaman live pada 10 September. Yang tercatat dari toko itu sendiri hanya dua: tombolnya diam tanpa request, dan errornya ada di store validasi.
Satu kolom kosong di alamat tersimpan
Sumber masalahnya ada di data. Akun uji yang dipakai untuk checkout membawa alamat tersimpan lama tanpa state, dan alamat itulah yang membuat Place Order diam. Catatan saya menjelaskannya begini: WooCommerce mewajibkan state untuk AE, kode negara Uni Emirat Arab, dan state itu berupa kolom teks.
Perbaikannya juga di data. Alamat tersimpan akun uji sekarang sudah diisi state "Dubai", dan tidak ada kode yang diubah untuk itu.
Kode 10.6.2 hanya mendukung separuh catatan itu
Kata "mewajibkan" di catatan tadi tidak bisa saya cocokkan dengan kode yang saya pegang. Di WooCommerce 10.6.2, get_country_locale() justru menulis state untuk AE sebagai tidak wajib:
// includes/class-wc-countries.php, WooCommerce 10.6.2
'AE' => array(
'postcode' => array(
'required' => false,
'hidden' => true,
),
'state' => array(
'required' => false,
),
),Nilai itu juga tidak berubah di jalan menuju checkout Blocks. CartCheckoutUtils membangun $country_locales dengan array_map atas get_country_locale(), dan fungsi di dalamnya hanya mengganti priority menjadi index serta membuang class. Hasilnya mengisi locale tiap negara di get_country_data(), lalu blok Checkout mendaftarkan data negara itu sebagai setting countryData. Flag required ikut lewat tanpa disentuh.
Bagian "kolom teks" lebih masuk akal. Di i18n/states.php 10.6.2 tidak ada daftar state untuk Uni Emirat Arab. Satu-satunya entri berkunci 'AE' di berkas itu adalah 'Armed Forces (AE)', dan letaknya di dalam daftar negara bagian AS. Tanpa daftar, memang tidak ada pilihan yang bisa ditawarkan lewat dropdown. Kalau kamu memeriksa kasus yang sama, ingat bahwa mencari kunci 'AE' di berkas itu akan mengantarmu ke Armed Forces, bukan ke Emirat.
Apakah state memang wajib di 11.1.0, dan kalau iya dari mana kewajiban itu datang, saya tidak punya buktinya, karena berkas yang sama di 11.1.0 belum saya bandingkan. Yang tidak ikut goyah adalah pengamatannya di toko.
Akun uji yang menanggung semua tes
Bug ini tinggal di data satu akun, jadi akun itu perlu diceritakan sedikit. Toko ini dibangun langsung di situs produksi sejak 30 Agustus 2026, dan semua mutasi keranjang serta order uji wajib lewat satu akun tester. Keranjang WooCommerce menempel per akun di server, dan memakai akun admin untuk uji pernah menimpa keranjang yang sedang saya pakai.
Pada 9 September ketahuan bahwa akun tester itu ternyata berperan administrator, bukan customer. Karena itu dia lolos mode coming-soon dan punya kapabilitas manage_woocommerce. Waktu itu toko masih coming-soon untuk publik, dan mode itu memblokir semua non-admin sementara private link tidak berfungsi, jadi tampilan bukan pemilik tidak bisa diuji live.
Buat bug ini, akibatnya jelas. Saya tidak bisa bilang seperti apa tombol yang diam itu di mata pembeli sungguhan, karena checkout live hanya terjangkau akun administrator dan akun uji itu sendiri administrator. Menurunkannya ke customer layak dilakukan supaya uji mewakili pembeli sungguhan, tapi waktu itu keputusannya belum saya ambil.
Yang sudah dipasang, yang belum terbukti
Perubahan alamat itu sudah dipasang. Yang tidak ada di catatan adalah uji ulang yang dikaitkan langsung dengan perubahan itu: tidak ada kalimat bahwa Place Order dicoba lagi lalu benar-benar mengirim request, dan tanggal perbaikannya juga tidak tercatat.
Catatan saya memang menyimpan order uji yang berhasil dengan akun tester, satu pada 3 September dan satu tes bayar penuh pada 10 September dini hari yang berakhir processing lewat webhook. Keduanya tidak dihubungkan dengan penambahan state, dan selama tanggal perbaikannya tidak diketahui, saya tidak memakainya sebagai bukti. Jadi statusnya: dipasang, belum terverifikasi oleh uji yang tercatat.
Kalau tombol bayar seperti mati lagi
Pelajaran yang tercatat dari kasus ini cuma satu kalimat: kalau tombol bayar seperti mati, cek wc/store/validation dulu. Dijabarkan sedikit, urutannya kira-kira begini.
- Pastikan dulu apakah ada request yang keluar saat tombol ditekan. Di toko ini, kegagalan dari sisi pembayaran datang dengan pesannya sendiri, seperti kartu yang ditolak tadi. Kalau tidak ada request sama sekali, masalahnya masih di browser.
- Baca store validasi dengan
hasValidationErrors()dangetValidationErrors(). Satu kunci saja sudah membuat yang pertama bernilai true. - Kalau checkout memakai alamat tersimpan, periksa datanya, bukan hanya tampilan formnya. Di kasus ini, satu kolom state yang kosong di alamat lama sudah cukup untuk membuat tombolnya diam.
- Cocokkan catatan soal field wajib, termasuk catatan sendiri, dengan kode versi yang sedang jalan. Catatan saya hanya didukung separuh oleh kode 10.6.2.