/**
* Geolocation-based product restrictions
* Compatible with all Shopify plans (no checkout scripts required)
* Version 3.4 - Clean rebuild with essential fixes only
*/
// Prevent duplicate initialization
if (!window.geoRestrictionsInitialized) {
window.geoRestrictionsInitialized = true;
class GeolocationRestrictions {
constructor() {
this.config = {
enabled: window.geoRestrictionSettings?.enabled || false,
allowedCountries: this.parseCountries(window.geoRestrictionSettings?.allowedCountries || 'US'),
restrictedMessage: window.geoRestrictionSettings?.restrictedMessage || 'This product is not available in your region.',
showContactLink: window.geoRestrictionSettings?.showContactLink || true,
productTemplate: window.geoRestrictionSettings?.productTemplate || 'default',
isCollectionPage: window.geoRestrictionSettings?.isCollectionPage || false,
cloudflareCountry: window.geoRestrictionSettings?.cloudflareCountry || null
};
this.userCountry = null;
this.storageKey = 'shopify_user_country';
this.storageExpiry = 'shopify_country_expiry';
}
parseCountries(countriesString) {
return countriesString
.toUpperCase()
.split(',')
.map(c => c.trim())
.filter(c => c.length === 2);
}
async init() {
console.log('=== GEO RESTRICTIONS INIT v3.4 ===');
console.log('Enabled:', this.config.enabled);
console.log('Product Template:', this.config.productTemplate);
console.log('Is Collection Page:', this.config.isCollectionPage);
if (!this.config.enabled) {
console.log('Geolocation restrictions disabled');
this.removeLoadingClass();
return;
}
if (this.config.productTemplate === 'records') {
console.log('Records template - no restrictions, showing all content');
this.removeLoadingClass();
this.showAllowedElements();
return;
}
if (window.CLOUDFLARE_COUNTRY) {
console.log('Using Cloudflare Worker country:', window.CLOUDFLARE_COUNTRY);
this.userCountry = window.CLOUDFLARE_COUNTRY;
this.setCachedCountry(this.userCountry);
this.applyRestrictions();
return;
}
// Note: Removed Shopify.country check - it reflects market selection, not actual visitor location
const cachedCountry = this.getCachedCountry();
if (cachedCountry) {
this.userCountry = cachedCountry;
this.applyRestrictions();
return;
}
await this.detectCountry();
this.applyRestrictions();
}
removeLoadingClass() {
document.documentElement.classList.remove('geo-checking');
document.documentElement.classList.add('geo-checked');
}
getCachedCountry() {
try {
const expiry = sessionStorage.getItem(this.storageExpiry);
const country = sessionStorage.getItem(this.storageKey);
if (expiry && country && Date.now() < parseInt(expiry)) {
console.log(`Using cached country: ${country}`);
return country;
}
} catch (e) {
console.warn('sessionStorage not available:', e);
}
return null;
}
setCachedCountry(country) {
try {
// Cache for 1 hour
const expiry = Date.now() + (60 * 60 * 1000);
sessionStorage.setItem(this.storageKey, country);
sessionStorage.setItem(this.storageExpiry, expiry.toString());
} catch (e) {
console.warn('Could not cache country:', e);
}
}
async detectCountry() {
try {
const response = await fetch('https://ipapi.co/json/', {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) throw new Error('Geolocation API failed');
const data = await response.json();
this.userCountry = data.country_code || 'US';
console.log(`Detected country: ${this.userCountry}`);
this.setCachedCountry(this.userCountry);
} catch (error) {
console.warn('Country detection failed, defaulting to US:', error);
this.userCountry = 'US';
}
}
isCountryAllowed() {
return this.config.allowedCountries.includes(this.userCountry);
}
collapseParentIfEmpty(el) {
el.style.margin = '0';
el.style.padding = '0';
el.style.height = '0';
el.style.minHeight = '0';
const parent = el.parentElement;
if (!parent) return;
const tagName = parent.tagName.toLowerCase();
const className = (parent.className || '').toLowerCase();
if (tagName !== 'div' && tagName !== 'span') return;
if (className.includes('product')) return;
if (className.includes('info')) return;
if (className.includes('content')) return;
if (className.includes('wrapper')) return;
if (className.includes('container')) return;
if (className.includes('form')) return;
if (className.includes('main')) return;
if (className.includes('section')) return;
if (className.includes('block') && !className.includes('text-block') && !className.includes('group-block')) return;
const children = Array.from(parent.children);
if (children.length === 1 && children[0] === el) {
parent.style.display = 'none';
parent.style.margin = '0';
parent.style.padding = '0';
parent.setAttribute('data-geo-restricted', 'true');
}
}
// Remove "- Unavailable" and "- Sold out" from variant selectors
cleanVariantSelectors() {
console.log('=== CLEANING VARIANT SELECTORS ===');
const cleanText = (text) => {
return text
.replace(/ - Unavailable/gi, '')
.replace(/ - Sold out/gi, '')
.replace(/ — Unavailable/gi, '')
.replace(/ — Sold out/gi, '')
.replace(/- Unavailable/gi, '')
.replace(/- Sold out/gi, '');
};
// Clean all select options - with whitespace normalization
document.querySelectorAll('select option').forEach(option => {
const originalText = option.textContent;
const normalizedText = originalText.replace(/\s+/g, ' ').trim();
if (normalizedText.toLowerCase().includes('unavailable') || normalizedText.toLowerCase().includes('sold out')) {
const cleaned = normalizedText
.replace(/\s*-\s*Unavailable/gi, '')
.replace(/\s*—\s*Unavailable/gi, '')
.replace(/\s*-\s*Sold out/gi, '')
.replace(/\s*—\s*Sold out/gi, '')
.trim();
if (cleaned !== normalizedText) {
option.setAttribute('data-original-text', originalText);
option.textContent = cleaned;
console.log('Cleaned option:', normalizedText, '->', cleaned);
}
}
});
// Clean variant picker displayed values
document.querySelectorAll('variant-picker span, variant-picker div, .variant-option span, [class*="variant"] span').forEach(el => {
if (el.children.length === 0) {
const text = el.textContent;
if ((text.includes('Unavailable') || text.includes('Sold out')) && text.length < 100) {
const cleaned = cleanText(text);
if (cleaned !== text) {
el.setAttribute('data-original-text', text);
el.textContent = cleaned;
console.log('Cleaned span:', text, '->', cleaned);
}
}
}
});
// Run again after delays
setTimeout(() => this.cleanVariantSelectorsDelayed(), 100);
setTimeout(() => this.cleanVariantSelectorsDelayed(), 500);
setTimeout(() => this.cleanVariantSelectorsDelayed(), 1500);
// Set up MutationObserver for dynamic changes
document.querySelectorAll('variant-picker, .variant-picker').forEach(picker => {
if (!picker.hasAttribute('data-geo-observed')) {
picker.setAttribute('data-geo-observed', 'true');
const observer = new MutationObserver(() => {
this.cleanVariantSelectorsDelayed();
});
observer.observe(picker, { childList: true, subtree: true, characterData: true });
}
});
}
cleanVariantSelectorsDelayed() {
document.querySelectorAll('select option').forEach(option => {
const originalText = option.textContent;
const normalizedText = originalText.replace(/\s+/g, ' ').trim();
if (normalizedText.toLowerCase().includes('unavailable') || normalizedText.toLowerCase().includes('sold out')) {
option.textContent = normalizedText
.replace(/\s*-\s*Unavailable/gi, '')
.replace(/\s*—\s*Unavailable/gi, '')
.replace(/\s*-\s*Sold out/gi, '')
.replace(/\s*—\s*Sold out/gi, '')
.trim();
}
});
document.querySelectorAll('variant-picker span, variant-picker div, .variant-option span').forEach(el => {
if (el.children.length === 0 && el.textContent) {
const text = el.textContent;
if ((text.includes('Unavailable') || text.includes('Sold out')) && text.length < 100) {
el.textContent = text
.replace(/ - Unavailable/gi, '')
.replace(/ - Sold out/gi, '')
.replace(/ — Unavailable/gi, '')
.replace(/ — Sold out/gi, '')
.replace(/- Unavailable/gi, '')
.replace(/- Sold out/gi, '');
}
}
});
}
applyRestrictions() {
this.removeLoadingClass();
if (this.config.productTemplate === 'records') {
console.log('Records template - skipping restrictions');
document.documentElement.classList.add('geo-allowed');
this.showAllowedElements();
return;
}
const allowed = this.isCountryAllowed();
console.log(`Country: ${this.userCountry}, Allowed: ${allowed}`);
if (!allowed) {
document.documentElement.classList.add('geo-restricted');
this.hideRestrictedElements();
// Note: showRestrictionMessage removed - geo-dealer-display.liquid handles this now
} else {
document.documentElement.classList.add('geo-allowed');
this.showAllowedElements();
}
}
hideRestrictedElements() {
console.log('=== HIDING RESTRICTED ELEMENTS ===');
console.log('Is Collection Page:', this.config.isCollectionPage);
// Hide "Sold Out" badges on collection pages
if (this.config.isCollectionPage) {
document.querySelectorAll('.product-badges__badge, .product-badges, [class*="badge"], .badge').forEach(el => {
const text = el.textContent.trim().toLowerCase();
if (text === 'sold out' || text === 'soldout' || text.includes('sold out')) {
el.style.display = 'none';
el.setAttribute('data-geo-hidden-badge', 'true');
}
});
}
// Hide add to cart text metafield blocks (short pricing notes)
document.querySelectorAll('.text-block, .group-block, rte-formatter, [class*="text-block"], [class*="rich-text"], .rte').forEach(el => {
const text = el.textContent.trim().toLowerCase();
if (text.length < 100) {
if (text.includes('price is per') ||
text.includes('price per') ||
text.includes('priced per') ||
text.includes('sold as pair') ||
text.includes('sold as a pair') ||
text === 'per pair' ||
text.includes('each unit') ||
text.includes('price includes')) {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
this.collapseParentIfEmpty(el);
}
}
});
// Hide prices
const priceSelectors = [
'.product__price', '.price', '.product-price', '[data-price]', '.money',
'.price--on-sale', '.price-item--sale', '.price-item--regular',
'.price__sale', '.price__regular', '.compare-at-price', '.was-price'
];
priceSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(el => {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
});
});
// Collection page: hide prices in cards
if (this.config.isCollectionPage) {
document.querySelectorAll('[class*="card"] .price, [class*="grid"] .price, .price s, .price del, s, del').forEach(el => {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
});
}
// Hide add to cart buttons
const cartSelectors = [
'button[name="add"]', '.product-form__submit', '.add-to-cart',
'form[action*="/cart/add"]', '.shopify-payment-button', '.product-form__buttons',
'.trade-in-buttons-wrapper', '.trade-in-button-group', '.trade-in-btn',
'.buy-buttons-block', '.product-form-buttons', '.add-to-cart-button',
'button[data-open-trade-modal]', 'button[data-add-to-cart-normal]', 'button[data-add-with-trade]'
];
cartSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(el => {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
});
});
// Hide quantity selectors
document.querySelectorAll('.product-form__quantity, .quantity-selector').forEach(el => {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
});
// Hide trade-in promo text
document.querySelectorAll('strong, p, div, span, section, aside').forEach(el => {
const text = el.textContent.trim();
if (text.length < 300) {
if ((text.startsWith('Receive up to') && text.includes('trade in')) ||
(text.includes('Now available') && text.includes('trade in')) ||
text.includes('Receive up to $') ||
text.includes('trade in your')) {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
this.collapseParentIfEmpty(el);
}
}
if (text === '-%}' || text.trim() === '-%}') {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
}
});
// Hide trade-in class elements
document.querySelectorAll('[class*="trade-in"], [class*="tradein"], [class*="trade-up"], [class*="tradeup"]').forEach(el => {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
});
// Hide accordion sections
document.querySelectorAll('accordion-custom, details').forEach(el => {
const text = el.textContent;
if (text.includes('Talk to a Hi-Fi Specialist') ||
text.includes('Home Audition Made Easy') ||
(text.includes('Shipping') && !text.includes('Shipping policy'))) {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
}
});
// Hide menu prices
document.querySelectorAll('.menu .price, .dropdown .price, .mega-menu .price, nav .price, header .price').forEach(el => {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
});
// Hide waiting list / waitlist buttons (for international visitors)
document.querySelectorAll('[class*="waiting"], [class*="waitlist"], [class*="Waiting"], [class*="Waitlist"]').forEach(el => {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
});
document.querySelectorAll('button, a.button, a[class*="btn"]').forEach(el => {
const text = el.textContent.toLowerCase();
if (text.includes('waiting list') || text.includes('waitlist') || text.includes('notify me')) {
el.style.display = 'none';
el.setAttribute('data-geo-restricted', 'true');
}
});
// Clean variant selectors (remove "- Unavailable")
this.cleanVariantSelectors();
}
showAllowedElements() {
const restrictionMsg = document.querySelector('.geo-restriction-message');
if (restrictionMsg) restrictionMsg.remove();
document.querySelectorAll('[data-geo-restricted]').forEach(el => {
el.style.display = '';
el.style.margin = '';
el.style.padding = '';
el.style.height = '';
el.style.minHeight = '';
el.style.overflow = '';
el.removeAttribute('data-geo-restricted');
});
document.querySelectorAll('[data-geo-hidden-badge]').forEach(el => {
el.style.display = '';
el.removeAttribute('data-geo-hidden-badge');
});
document.querySelectorAll('[data-original-text]').forEach(el => {
el.textContent = el.getAttribute('data-original-text');
el.removeAttribute('data-original-text');
});
}
setTestCountry(countryCode) {
this.userCountry = countryCode.toUpperCase();
this.setCachedCountry(this.userCountry);
document.documentElement.classList.remove('geo-restricted', 'geo-allowed');
document.querySelectorAll('[data-geo-restricted]').forEach(el => {
el.style.display = '';
el.style.margin = '';
el.style.padding = '';
el.style.height = '';
el.style.minHeight = '';
el.style.overflow = '';
el.removeAttribute('data-geo-restricted');
});
document.querySelectorAll('[data-geo-hidden-badge]').forEach(el => {
el.style.display = '';
el.removeAttribute('data-geo-hidden-badge');
});
document.querySelectorAll('[data-original-text]').forEach(el => {
el.textContent = el.getAttribute('data-original-text');
el.removeAttribute('data-original-text');
});
const msg = document.querySelector('.geo-restriction-message');
if (msg) msg.remove();
this.applyRestrictions();
this.removeLoadingClass();
}
clearCache() {
try {
sessionStorage.removeItem(this.storageKey);
sessionStorage.removeItem(this.storageExpiry);
// Also clear old localStorage entries
localStorage.removeItem(this.storageKey);
localStorage.removeItem(this.storageExpiry);
console.log('Country cache cleared');
} catch (e) {
console.warn('Could not clear cache:', e);
}
}
}
function initGeoRestrictions() {
if (!window.geoRestrictionSettings) {
console.warn('Waiting for geoRestrictionSettings...');
setTimeout(initGeoRestrictions, 50);
return;
}
console.log('Initializing geo restrictions v3.4');
window.geoRestrictions = new GeolocationRestrictions();
window.geoRestrictions.init();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initGeoRestrictions);
} else {
initGeoRestrictions();
}
window.testGeoRestriction = function(countryCode) {
if (window.geoRestrictions) {
window.geoRestrictions.setTestCountry(countryCode);
}
};
window.clearGeoCache = function() {
if (window.geoRestrictions) {
window.geoRestrictions.clearCache();
location.reload();
}
};
}
Skip to content
Have you ever noticed how outside appearances often set our opinion on what's inside? An angry or friendly look on someone's face sets the stage for what we think is going on inside.
While this makes sense in humans because our outer appearance often is a reflection of what's inside, it doesn't translate well to machines. A frustrating user interface doesn't tell you how well the hardware functions.
This is especially frustrating when it comes to our personal music systems. If the volume control's a pain to use, or the sample rate indicator is wrong, we get all worked up and find it hard to enjoy the music. Which perhaps explains why today, our engineering team spends as much time designing the user interface as we do the actual hardware producing the music.
While I get it, I must say it's a bit frustrating. Having grown up in this industry where the UI consisted of little more than a series of knobs, switches and buttons—controls that we maybe took a day or two to design, there was a certain beauty to investing all our efforts into performance.
It's not that we don't still do that. The difference today is that it simply takes twice as long to get from an idea to a finished product we're proud to put the PS name on.
But it's not just audio equipment that's laboring under these new paradigms.
The next time you're shopping for a new fridge and get enamored with its video screen running on AI, give a nod to the good old days where there was not much more than a handle to open it.
The food inside tastes the same regardless of the amount of lipstick applied to the exterior.
💬
Join the Conversation
Share your thoughts below and get our latest posts in your inbox
We respect your privacy. Unsubscribe anytime.
0 comments