1/** 2 * Sync country-dependent UI with the country WCPBC actually resolves on 3 * the server. Targets: 4 * - #shipping-to and .footer-shipping-to (header/footer country pill) 5 * - body country--XX class 6 * - PDP .pdp-above-addtocart__tax and .pdp-below-addtocart__text 7 * 8 * Page cache (WP Rocket) may serve HTML where these reflect whichever 9 * country the cached variant used, not the visitor's. The endpoint 10 * (nr_resolved_country) reads the live customer session and returns 11 * the resolved country/zone plus a shipping bundle (freeShipping, 12 * taxCollected, deliveryTimeMin/Max). 13 * 14 * Strategy â cache + zone-change revalidation: 15 * 1. First visit (no cache): call endpoint, cache the response. 16 * 2. Subsequent visits: apply cached value synchronously. 17 * 3. wc_price_based_country_after_ajax_geolocation fires with 18 * zone_id â if it differs from the cached zone, revalidate. 19 * 4. The [country_selector] submit handler in functions.php clears 20 * sessionStorage so the next page is a "first visit." 21 * 5. The cached bundle carries a rate_sig fingerprinting the zone 22 * pricing config. If it differs from the one the server printed 23 * into this page, the cache predates a rate change and is dropped. 24 * Needed because editing a zone's exchange rate doesn't change its 25 * zone_id, so step 3 alone would never notice. 26 */ 27(function ($) { 28 if (typeof nrResolvedCountry === 'undefined' || !nrResolvedCountry.endpoint) { 29 document.body.classList.add('nr-country-resolved'); 30 return; 31 } 32 33 var ENDPOINT = nrResolvedCountry.endpoint; 34 var STORAGE_KEY = 'nrResolvedCountry'; 35 // Fingerprint of the zone pricing config (exchange rates + rounding)
36 // that the server printed into THIS page. A cached bundle carrying a 37 // different signature predates a rate change and must be discarded â 38 // zone_id alone can't detect that, since editing a zone's rate leaves 39 // its id untouched. 40 var RATE_SIG = nrResolvedCountry.rate_sig || ''; 41 var REVEAL_CLASS = 'nr-country-resolved'; 42 var REVEAL_TIMEOUT_MS = 1500; 43 44 function readCache() { 45 try { 46 var raw = window.sessionStorage.getItem(STORAGE_KEY); 47 if (!raw) return null; 48 var parsed = JSON.parse(raw); 49 if (!parsed || typeof parsed !== 'object' || !parsed.country) return null; 50 // Stale pricing config â treat as a cache miss so we refetch. 51 if (RATE_SIG) { 52 var cachedSig = parsed.currency_format && parsed.currency_format.rate_sig; 53 if (cachedSig !== RATE_SIG) return null; 54 } 55 return parsed; 56 } catch (e) { 57 return null; 58 } 59 } 60 61 function writeCache(info) { 62 try { 63 window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(info)); 64 } catch (e) { /* quota / disabled â ignore */ } 65 // Notify same-tab listeners (storage event only fires cross-tab). 66 try { 67 document.dispatchEvent(new CustomEvent('nr:resolved-country', { detail: info })); 68 } catch (e) { /* old browsers â ignore */ } 69 } 70 71 function reveal() { 72 document.body.classList.add(REVEAL_CLASS); 73 } 74 75 /** 76 * Reconcile .country-active on the [country_selector] shortcode. 77 * PHP marks the active country at render time from 78 * nr_get_customer_country(), which can be stale on cached pages. 79 * We strip any pre-existing active flag and re-apply it to the 80 * row matching the resolved country. 81 */ 82 function applyCountrySelectorActive(info) { 83 document.querySelectorAll('.country-selector .country').forEach(function (row) { 84 row.classList.remove('country-active'); 85 }); 86 if (!info.country) return; 87 document.querySelectorAll('.country-selector .country-' + info.country).forEach(function (row) { 88 row.classList.add('country-active'); 89 }); 90 } 91 92 /** 93 * Drive the conditional-hide-country--XX / conditional-display-country--XX 94 * CSS hooks. PHP would have rendered this with the cookie-based country, 95 * which is stale on cached pages â write it client-side instead. 96 */ 97 function applyConditionalCountryCss(info) { 98 var styleEl = document.getElementById('nr-country-conditional-css'); 99 if (!styleEl) return; 100 if (!info.country) { 101 styleEl.textContent = ''; 102 return; 103 } 104 var c = info.country.replace(/[^A-Z]/g, ''); 105 if (!c) return; 106 styleEl.textContent = 107 '.conditional-hide-country--' + c + ',' + 108 '[class*="conditional-display-country--"]:not(.conditional-display-country--' + c + ')' + 109 '{display:none}'; 110 } 111 112 function applyShippingPill(info) { 113 document.querySelectorAll('#shipping-to').forEach(function (el) { 114 if (info.country_name) el.setAttribute('data-country', info.country_name); 115 var a = el.querySelector('.CountryPopup'); 116 if (a && info.display) a.textContent = info.display; 117 }); 118 119 document.querySelectorAll('.footer-shipping-to .CountryPopup').forEach(function (a) { 120 if (!info.display) return; 121 var spans = a.querySelectorAll('span'); 122 if (spans.length) { 123 spans[0].textContent = info.display; 124 } else { 125 a.textContent = info.display; 126 } 127 }); 128 129 document.body.className = document.body.className 130 .replace(/\bcountry--[A-Z]{2}\b/g, '') 131 .replace(/\s+/g, ' ') 132 .trim(); 133 document.body.classList.add('country--' + info.country); 134 } 135 136 /** 137 * Update the PDP <script id="nr-product-info-data"> JSON so any later 138 * reader (or re-render) sees the resolved shipping bundle. 139 */ 140 function patchPdpDataJson(info) { 141 if (!info.shipping) return; 142 var dataScript = document.getElementById('nr-product-info-data'); 143 if (!dataScript) return; 144 145 var data; 146 try { data = JSON.parse(dataScript.textContent || '{}'); } 147 catch (e) { return; } 148 149 data.shipping = data.shipping || {}; 150 data.shipping.freeShipping = !!info.shipping.freeShipping; 151 data.shipping.taxCollected = !!info.shipping.taxCollected; 152 data.shipping.deliveryTimeMin = info.shipping.deliveryTimeMin; 153 data.shipping.deliveryTimeMax = info.shipping.deliveryTimeMax; 154 data.shipping.country = info.country; 155 data.shipping.countryName = info.country_name; 156 157 dataScript.textContent = JSON.stringify(data); 158 } 159 160 /** 161 * Re-render the two PDP blocks directly. Mirrors the markup 162 * produced by main.js (injectAboveAddToCart / injectBelowAddToCart)
163 * so the result is identical regardless of which ran first. 164 */ 165 function renderPdpBlocks(info) { 166 if (!info.shipping) return; 167 168 var aboveTax = document.querySelector('.pdp-above-addtocart .pdp-above-addtocart__tax'); 169 if (info.shipping.taxCollected) { 170 if (!aboveTax) { 171 var aboveInner = document.querySelector('.pdp-above-addtocart .pdp-above-addtocart__inner'); 172 if (aboveInner) { 173 aboveTax = document.createElement('div'); 174 aboveTax.className = 'pdp-above-addtocart__tax'; 175 aboveTax.innerHTML = 176 '<img src="/wp-content/themes/norwegianrain/images/checkmark.svg" alt="" class="pdp-icon">' + 177 '<span>All taxes and duties included</span>'; 178 aboveInner.insertBefore(aboveTax, aboveInner.firstChild); 179 } 180 } 181 } else if (aboveTax) { 182 aboveTax.remove(); 183 } 184 185 var belowText = document.querySelector('.pdp-below-addtocart .pdp-below-addtocart__text'); 186 if (belowText) { 187 var line1 = belowText.querySelector('.pdp-below-addtocart__line1'); 188 var line1Text = info.shipping.freeShipping 189 ? 'Free shipping & free returns' 190 : 'Return or exchange any product within 30 days.'; 191 192 if (line1) { 193 line1.textContent = line1Text; 194 } else { 195 line1 = document.createElement('div'); 196 line1.className = 'pdp-below-addtocart__line1'; 197 line1.textContent = line1Text; 198 belowText.insertBefore(line1, belowText.firstChild); 199 } 200 201 var line2Text = 'Worldwide Express delivery with DHL'; 202 if (info.country_name && info.shipping.deliveryTimeMin && info.shipping.deliveryTimeMax) { 203 line2Text += ' (' + info.shipping.deliveryTimeMin + '-' + info.shipping.deliveryTimeMax + ' days to ' + info.country_name + ')'; 204 } 205 var line2 = belowText.querySelector('.pdp-below-addtocart__line2'); 206 if (line2) { 207 line2.textContent = line2Text; 208 } else { 209 line2 = document.createElement('div'); 210 line2.className = 'pdp-below-addtocart__line2'; 211 line2.textContent = line2Text; 212 belowText.appendChild(line2); 213 } 214 } 215 } 216 217 function apply(info) { 218 if (!info || !info.country) { 219 reveal(); 220 return; 221 } 222 223 applyConditionalCountryCss(info); 224 applyShippingPill(info); 225 patchPdpDataJson(info); 226 227 // PDP blocks may be rendered by main.js on DOMContentLoaded â 228 // run on both ready states to cover early and late execution. 229 // .country-active on the [country_selector] shortcode is set 230 // by PHP at render time, so we reconcile it here too. 231 if (document.readyState === 'loading') { 232 document.addEventListener('DOMContentLoaded', function () { 233 renderPdpBlocks(info); 234 applyCountrySelectorActive(info); 235 }); 236 } else { 237 renderPdpBlocks(info); 238 applyCountrySelectorActive(info); 239 } 240 241 reveal(); 242 } 243 244 function fetchAndApply(newZone) { 245 $.ajax({ 246 url: ENDPOINT, 247 type: 'GET', 248 cache: false, 249 headers: { 'Cache-Control': 'no-cache, max-age=0' }, 250 success: function (info) { 251 if (!info || !info.country) { 252 reveal(); 253 return; 254 } 255 if (newZone) info.zone = newZone; 256 writeCache(info); 257 apply(info); 258 }, 259 error: reveal 260 }); 261 } 262 263 var cached = readCache(); 264 var revealTimer; 265 266 if (cached) { 267 apply(cached); 268 } else { 269 revealTimer = window.setTimeout(reveal, REVEAL_TIMEOUT_MS); 270 fetchAndApply(); 271 } 272 273 $(document.body).on('wc_price_based_country_after_ajax_geolocation', function (e, zoneId) { 274 if (revealTimer) window.clearTimeout(revealTimer); 275 276 var current = readCache(); 277 if (!current) return; // first-visit fetch still in flight 278 if (current.zone && zoneId && current.zone === zoneId) return; 279 280 fetchAndApply(zoneId || null); 281 }); 282})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.