From 535dbda6b48171f8115b455e54790de94d97d42a Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Sun, 26 Jul 2026 16:41:48 -0300 Subject: Initial import --- assets/js/cepis-woocommerce-blocks.js | 243 ++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 assets/js/cepis-woocommerce-blocks.js (limited to 'assets/js/cepis-woocommerce-blocks.js') diff --git a/assets/js/cepis-woocommerce-blocks.js b/assets/js/cepis-woocommerce-blocks.js new file mode 100644 index 0000000..bcbab93 --- /dev/null +++ b/assets/js/cepis-woocommerce-blocks.js @@ -0,0 +1,243 @@ +/** + * WooCommerce Cart/Checkout Blocks. + * + * The block checkout is React and its address inputs are controlled: a value + * written into the DOM from outside is discarded on the next render, and the + * `wc/store/cart` data store — which is what the order is actually built from — + * never hears about it. So nothing here touches a field. The postcode is read + * from the data store, and the resolved address is written back through the + * store's own `setShippingAddress` / `setBillingAddress` actions, which is the + * documented extension point. React then re-renders the inputs itself. + * + * Only the request layer is shared with the classic integration: + * `CepisAutofill.lookup` is the widget's HTTP client without its DOM half — + * same cache, same error codes, same handling of 400, 404, 429 and 5xx. + * + * @module + */ + +(function () { + var config = window.cepisWooCommerce; + var data = window.wp && window.wp.data; + + if (!config || !config.fields || !window.CepisAutofill || !data || !data.select) { + return; + } + + var CART = "wc/store/cart"; + var NOTICES = "core/notices"; + + /** + * Where a notice has to be rendered for the Checkout block to show it. + */ + var NOTICE_CONTEXT = "wc/checkout"; + + /** + * One id, so a new failure replaces the previous message instead of stacking. + */ + var NOTICE_ID = "cepis-woocommerce-lookup"; + + var SECTIONS = { + shipping: { address: "shippingAddress", write: "setShippingAddress" }, + billing: { address: "billingAddress", write: "setBillingAddress" }, + }; + + var progress = { + shipping: { cep: "", timer: 0, written: {} }, + billing: { cep: "", timer: 0, written: {} }, + }; + + /** + * The one field the shopper is never asked about. + * + * A street name is worth protecting — the shopper may know better than the + * database. A state is not: the CEP decides it. WooCommerce also starts the + * checkout with the store's own state already selected, which the `"auto"` + * rule would read as the shopper's choice and never touch, quietly filing a + * Rio address under São Paulo. + */ + var ALWAYS = "uf"; + + /** + * Whether a field may be written. + * + * Mirrors the widget's `overwrite` option: `"auto"` protects anything the + * shopper typed but replaces what a previous lookup wrote, so correcting the + * street by hand survives the next CEP. + * + * @param {string} field The API's name for the field. + * @param {string} current Value the store holds now. + * @param {string|undefined} written Value this plugin last wrote there. + * @returns {boolean} + */ + function writable(field, current, written) { + if (field === ALWAYS || config.overwrite === true) { + return true; + } + if (current.trim() === "") { + return true; + } + if (config.overwrite === false) { + return false; + } + return written === current; + } + + var pending = {}; + + /** + * Look a CEP up once, however many sections want it. + * + * With "use the same address for billing" on — the default — the shipping + * and the billing observer see the same postcode land in the same tick. The + * widget's own cache is only filled once a response is back, so without this + * the store would send two identical requests for every CEP a shopper types. + * + * @param {string} digits The eight digits. + * @returns {Promise>} + */ + function resolve(digits) { + if (!pending[digits]) { + var forget = function () { + delete pending[digits]; + }; + + pending[digits] = window.CepisAutofill.lookup(digits, { + baseUrl: config.baseUrl, + timeoutMs: config.timeoutMs, + }); + pending[digits].then(forget, forget); + } + + return pending[digits]; + } + + /** + * Show one message in the Checkout block's own notice area. + * + * @param {string} text Message, already translated. + */ + function announce(text) { + data.dispatch(NOTICES).createNotice("error", text, { + id: NOTICE_ID, + context: NOTICE_CONTEXT, + isDismissible: true, + }); + } + + /** Take the message away. */ + function silence() { + data.dispatch(NOTICES).removeNotice(NOTICE_ID, NOTICE_CONTEXT); + } + + /** + * Write a resolved address into the store. + * + * @param {string} name Section name, `shipping` or `billing`. + * @param {Record} address The API's answer. + */ + function apply(name, address) { + var customer = data.select(CART).getCustomerData(); + var current = (customer && customer[SECTIONS[name].address]) || {}; + var written = progress[name].written; + var patch = {}; + var changed = false; + + Object.keys(config.fields).forEach(function (field) { + var key = config.fields[field]; + var value = address[field]; + + if (!key || typeof value !== "string" || value === "") { + return; + } + + var held = current[key] == null ? "" : String(current[key]); + if (!writable(field, held, written[key])) { + return; + } + + patch[key] = value; + written[key] = value; + changed = true; + }); + + if (changed) { + data.dispatch(CART)[SECTIONS[name].write](patch); + } + } + + /** + * Resolve one CEP and write what comes back. + * + * @param {string} name Section name, `shipping` or `billing`. + * @param {string} digits The eight digits. + */ + function run(name, digits) { + silence(); + + resolve(digits).then( + function (address) { + if (progress[name].cep === digits) { + apply(name, address); + } + }, + function (error) { + if (progress[name].cep !== digits || error.code === "aborted") { + return; + } + announce(config.messages[error.code] || error.message); + }, + ); + } + + /** + * React to whatever the store now holds for one address. + * + * @param {string} name Section name, `shipping` or `billing`. + * @param {Record|undefined} address Address from the store. + */ + function observe(name, address) { + var seen = progress[name]; + + if (!address || String(address.country || "") !== "BR") { + return; + } + + var digits = String(address.postcode || "").replace(/\D+/g, ""); + + if (digits.length !== 8) { + if (seen.cep !== "") { + seen.cep = ""; + window.clearTimeout(seen.timer); + silence(); + } + return; + } + + if (digits === seen.cep) { + return; + } + + seen.cep = digits; + window.clearTimeout(seen.timer); + seen.timer = window.setTimeout(function () { + run(name, digits); + }, config.debounceMs); + } + + data.subscribe(function () { + var store = data.select(CART); + if (!store || typeof store.getCustomerData !== "function") { + return; + } + + var customer = store.getCustomerData(); + if (!customer) { + return; + } + + Object.keys(SECTIONS).forEach(function (name) { + observe(name, customer[SECTIONS[name].address]); + }); + }, CART); +})(); -- cgit v1.3