// Kviq
const SCRIPT_FILENAME = 'kviq-buy.js';
const BUY_BTN_CLASS = '_kviq_buy';
const KVIQ_ID = ( window.location.search.match(/[?&]_k=([^&]+)/) || [ , '' ] )[1];
const ROOT_DOMAIN = 'https://s2.kviq.no';
const POLL_INTERVAL = 5000;
const POLL_TIMEOUT = 60000;
//const STORES_DEBUGGING = [ 8093139025, 64014647361, 25135185971 ];
const STORES_DEBUGGING = [ 8093139025, 25135185971 ];
let KVIQ_TESTING = false;
let KVIQ_LOADING = false;
let paymentTimer = null;
let storeId = 'unknown';
let kviq_buy_button, kviq_buy_button_live, kviq_buy_button_loading;
if ( document.readyState === 'loading' ){
document.addEventListener( 'DOMContentLoaded', initKviq );
} else {
initKviq();
}
// Fix cleanup
window.addEventListener('beforeunload', () => {
if (paymentTimer) clearInterval(paymentTimer);
});
// helper must come early
async function jsonPost(url, body){
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type':'application/json' },
body: JSON.stringify(body)
});
const data = await res.json().catch(() => ({ error: 'Invalid JSON' }));
if (!res.ok || !data.vippsRedirectUrl){
throw new Error(data.error || 'Request failed');
}
return data;
}
(function(){
const params = new URLSearchParams( window.location.search );
const keys = [ 'utm_source','utm_medium','utm_campaign','utm_term','utm_content','utm_id','utm_channel','fbclid','gclid','gbraid','wbraid','ttclid','li_fat_id','msclkid','yclid','dclid','twclid','snclid','ref','referrer','affiliate_id','partner','campaign_id' ];
let data;
try {
data = JSON.parse( localStorage.getItem('kviq_tracking') ) || {};
} catch {
data = { http_referrer: document.referrer || null };
}
let updated = false;
keys.forEach( k => {
const v = params.get(k);
if ( !v ){
return;
} else {
const existing = data[k];
if ( !existing || existing.value !== v ){
data[k] = {
value: v,
url: window.location.href,
timestamp: new Date().toISOString()
};
updated = true;
}
}
});
if ( updated ){
data.last_update = new Date().toISOString();
try {
localStorage.setItem('kviq_tracking', JSON.stringify(data));
} catch (e) {
console.warn('Failed to save tracking data:', e);
}
}
})();
function initKviq(){
const scriptTag = Array.from( document.querySelectorAll('script') ).find( s => s.src && s.src.includes(SCRIPT_FILENAME) );
storeId = (()=>{
try {
const url = new URL( scriptTag.src );
const param = url.searchParams.keys().next().value;
if ( param && /^\d+$/.test(param) ){
return param;
} else {
if ( scriptTag?.dataset?.storeId ){
return scriptTag.dataset.storeId;
} else {
return 'unknown';
}
}
} catch {
return 'unknown';
}
})();
// Debug flag logic
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('kviq_testing')) {
const debugValue = urlParams.get('kviq_testing');
if (debugValue === 'on') {
localStorage.setItem('KVIQ_TESTING', '1');
} else if (debugValue === 'off') {
localStorage.removeItem('KVIQ_TESTING');
}
}
KVIQ_TESTING = !!localStorage.getItem('KVIQ_TESTING');
const storeLanguage = scriptTag?.dataset?.lang || 'en';
kviq_buy_button_live = `Kjøp nå med `;
kviq_buy_button_loading = ``;
kviq_buy_button = kviq_buy_button_live;
function observeCartDrawer(){
const obs = new MutationObserver(()=>{
const cartBtn = document.querySelector('._kviq_cart_button');
if ( cartBtn ){
if (!cartBtn.classList.contains('_kviq_ready')) {
activateKviqButton('._kviq_cart_button', 'cart', async ()=>{
const res = await fetch('/cart.js',{credentials:'same-origin'});
const cart = await res.json();
return cart.items.map(i=>({variant_id:i.id,quantity:i.quantity}));
});
}
}
});
obs.observe( document.body, { childList:true, subtree:true } );
}
function checkPaymentStatus(){
if ( KVIQ_ID ){
fetch( ROOT_DOMAIN +'/api/vipps-status', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ ref: KVIQ_ID })
})
.then(r => r.json())
.then(d => {
if ( d ){
// Restore buttons if error, timeout
if ( d.status === 'timeout' || d.status === 'error' || d.status === 'aborted' ){
if ( paymentTimer ){
clearInterval( paymentTimer );
}
document.querySelectorAll('._kviq_buynow_button, ._kviq_cart_button').forEach( btn => {
btn.classList.remove('loading');
kviq_buy_button = kviq_buy_button_live;
btn.innerHTML = kviq_buy_button;
});
KVIQ_LOADING = false;
} else {
if ( ( d.status === 'redirect' || d.status === 'done' ) && d.data.shopify_order_url ){
fetch('/cart/clear.js', { method: 'POST', credentials: 'same-origin' }).then(() => console.log('Cart cleared')).catch(e => console.warn('Cart clear failed:', e));
window.location.href = ROOT_DOMAIN +'/r?'+ encodeURIComponent( d.data.shopify_order_url );
}
}
}
})
.catch(e => {
console.error('Vipps status error:', e);
});
}
}
activateKviqButton('._kviq_buynow_button', 'product', async ()=>{
const form = document.querySelector('form[action*="/cart/add"]');
const id = form?.querySelector('[name="id"]')?.value || new URLSearchParams(location.search).get('variant');
const q = form?.querySelector('[name="quantity"]')?.value || 1;
return [ { variant_id: id, quantity: parseInt(q,10) } ];
});
activateKviqButton('._kviq_cart_button', 'cart', async ()=>{
const res = await fetch('/cart.js',{credentials:'same-origin'});
const cart = await res.json();
return cart.items.map(i=>({variant_id:i.id,quantity:i.quantity}));
});
observeCartDrawer();
if (KVIQ_ID) {
const buttons = document.querySelectorAll('._kviq_buynow_button, ._kviq_cart_button');
let timeoutId = null;
// Update button states to show loading
buttons.forEach(btn => {
if (!btn.classList.contains('loading')) {
btn.dataset.originalHtml ??= btn.innerHTML; // Store original HTML
btn.classList.add('loading');
btn.innerHTML = kviq_buy_button_loading;
}
});
KVIQ_LOADING = true;
// Clear any existing timeout
if (timeoutId) clearTimeout(timeoutId);
async function pollPaymentStatus(attempt = 1, maxAttempts = POLL_TIMEOUT / POLL_INTERVAL) {
try {
await checkPaymentStatus(); // Assume this function handles status checks and redirects
// Schedule next check if within attempt limit
if (attempt < maxAttempts) {
timeoutId = setTimeout(() => pollPaymentStatus(attempt + 1, maxAttempts), POLL_INTERVAL);
} else {
// Max attempts reached, reset buttons
console.log('Payment check expired after 60 seconds');
buttons.forEach(btn => {
btn.classList.remove('loading');
btn.innerHTML = btn.dataset.originalHtml || kviq_buy_button_live;
});
KVIQ_LOADING = false;
}
} catch (error) {
console.error('Payment status check failed:', error);
// Optionally retry on error or reset immediately
buttons.forEach(btn => {
btn.classList.remove('loading');
btn.innerHTML = btn.dataset.originalHtml || kviq_buy_button_live;
});
KVIQ_LOADING = false;
}
}
// Start the first check
pollPaymentStatus();
} else {
// Improved click handler: Use event delegation on the product form instead of body for better scoping
const productForm = document.querySelector('form[action*="/cart/add"]');
if (productForm) {
productForm.addEventListener('click', e => {
const btn = e.target.closest('._kviq_buynow_button');
if (!btn || btn.classList.contains('_kviq_ready')) return;
const variantInput = productForm.querySelector('[name="id"]');
if (variantInput) {
btn.dataset.variantid = variantInput.value;
}
});
// Optimized variant observer: Narrow to observe only the variant input's attributes/subtree
const variantInput = productForm.querySelector('[name="id"]');
if (variantInput) {
const variantObserver = new MutationObserver(() => {
document.querySelectorAll('._kviq_buynow_button').forEach(btn => {
btn.dataset.variantid = variantInput.value;
});
});
variantObserver.observe(variantInput, { attributes: true, attributeFilter: ['value'] });
}
}
// Replace polling with a MutationObserver for cart button activation
function observeCartForButton() {
const cartObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
const cartBtn = document.querySelector('._kviq_cart_button:not(._kviq_ready)');
if (cartBtn) {
activateKviqButton('._kviq_cart_button', 'cart', async () => {
const res = await fetch('/cart.js', { credentials: 'same-origin' });
const cart = await res.json();
return cart.items.map(i => ({
variant_id: i.id,
quantity: i.quantity
}));
});
}
}
});
});
// Observe a more specific cart container if possible; fallback to body
const cartContainer = document.querySelector('#cart-drawer') || document.body; // Adjust #cart-drawer to your theme's selector
cartObserver.observe(cartContainer, { childList: true, subtree: true });
}
// Call the observer setup once
observeCartForButton();
}
}
function applyKviqButtonStyle(btn) {
const bgColor = btn?.dataset?.bgcolor || '#ff5b24';
const textColor = btn?.dataset?.textcolor || '#ffffff';
const borderRad = btn?.dataset?.borderradius || '0px';
const marginTop = btn?.dataset?.margintop || '0px';
const fontSize = btn?.dataset?.fontsize || '16px';
const fontWeight = btn?.dataset?.fontweight || '600';
const heightVal = btn?.dataset?.height || '50px';
const widthVal = btn?.dataset?.width || '100%';
const gapSize = btn?.dataset?.gap || '6px';
const shouldHide = !KVIQ_TESTING && STORES_DEBUGGING.includes(parseInt(storeId, 10));
Object.assign(btn.style, {
backgroundColor: bgColor,
color: textColor,
borderRadius: borderRad,
marginTop: marginTop,
height: heightVal,
width: widthVal,
alignItems: 'center',
justifyContent: 'center',
gap: gapSize,
cursor: 'pointer',
fontWeight: fontWeight,
fontSize: fontSize,
display: shouldHide ? 'none' : 'flex'
});
}
function activateKviqButton(selector, origin, getOrder){
const btn = document.querySelector(selector);
if (!btn || btn.classList.contains('_kviq_ready')) return;
btn.classList.add('_kviq_ready','button');
applyKviqButtonStyle(btn);
btn.setAttribute('role','button');
btn.setAttribute('tabindex','0');
btn.innerHTML = kviq_buy_button;
btn.addEventListener('click', async (e)=>{
e.preventDefault();
e.stopPropagation();
if (KVIQ_LOADING) return;
KVIQ_LOADING = true;
btn.classList.add('loading');
kviq_buy_button = kviq_buy_button_loading;
btn.innerHTML = kviq_buy_button;
try {
const order = await getOrder();
const tracking = JSON.parse( localStorage.getItem('kviq_tracking') || '{}' );
const data = await jsonPost( ROOT_DOMAIN +'/api/vipps-init', {
store_id: storeId,
origin: origin,
order: order,
tracking: tracking,
returnUrl: window.location.href
});
window.location.href = ROOT_DOMAIN +'/r?'+ encodeURIComponent( data.vippsRedirectUrl );
} catch (err){
console.error('Kviq '+ origin +' button error:', err);
alert(err.message || 'Network error.');
restoreKviqButtons();
}
});
}
function restoreKviqButtons(){
document.querySelectorAll( '._kviq_buynow_button, ._kviq_cart_button' ).forEach( btn=>{
btn.classList.remove( 'loading' );
kviq_buy_button = kviq_buy_button_live;
btn.innerHTML = kviq_buy_button;
});
KVIQ_LOADING = false;
}