/** * 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); } /** * Say which municipality a CEP that does not exist would have been in. * * Replaces the notice already on screen — same id, so the block swaps the * text rather than stacking a second error. The failure is reported * immediately and refined a round trip later, because making the shopper wait * on a second request before being told anything at all is worse than the * message improving under them. * * Nothing here goes near `apply()`. A CEP the Correios do not have has no * address, and the municipality in a 404 is a guess about which digit went * wrong, not a place to ship to. * * @param {string} name Section name, `shipping` or `billing`. * @param {string} digits The CEP that was not found. * @param {string} shown The message already in the notice area. */ function explain(name, digits, shown) { var helper = window.cepisWooCommerceFaixa; if (!helper) { return; } helper.message(digits, shown, function (message) { if (message !== shown && progress[name].cep === digits) { announce(message); } }); } /** * 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; } var shown = config.messages[error.code] || error.message; announce(shown); if (error.code === "not_found") { explain(name, digits, shown); } }, ); } /** * 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); })();