PageSourceSearch

https://norwegianrain.com/wp-content/themes/norwegianrain/js/nr-resolved-country.js?ver=8.18.1

js norwegianrain.com collected 2026-09-25 15:02:57 UTC 9,675 bytes, 282 lines download raw bytes

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.