1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
|
/**
* 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<Record<string, any>>}
*/
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<string, any>} 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<string, any>|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);
})();
|