// ==UserScript==
// @name Trumf Bonusvarsler Lite
// @description Trumf Bonusvarsler Lite er et minimalistisk userscript (Firefox, Safari, Chrome) som gir deg varslel når du er inne på en nettbutikk som gir Trumf-bonus.
// @namespace https://github.com/NewsGuyTor/Trumf-Bonusvarsler-Lite
// @version 1.2.1
// @match *://*/*
// @grant GM.xmlHttpRequest
// @connect wlp.tcb-cdn.com
// @connect raw.githubusercontent.com
// @homepageURL https://github.com/NewsGuyTor/Trumf-Bonusvarsler-Lite
// @supportURL https://github.com/NewsGuyTor/Trumf-Bonusvarsler-Lite/issues
// @icon https://github.com/NewsGuyTor/Trumf-Bonusvarsler-Lite/raw/main/icon.png
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ---------------------------
// Configuration
// ---------------------------
const FEED_URL = `https://wlp.tcb-cdn.com/trumf/notifierfeed.json?v=${Date.now()}`;
const FEED_FALLBACK_URL = "https://raw.githubusercontent.com/NewsGuyTor/Trumf-Bonusvarsler-Lite/main/sitelist.json";
const LOCAL_KEY_DATA = "TrumpBonusvarslerLiteFeedData";
const LOCAL_KEY_TIME = "TrumpBonusvarslerLiteFeedTimestamp";
const CACHE_DURATION = 1000 * 60 * 60 * 6; // 6 hours
const WIDTH_THRESHOLD = 700; // px
const MESSAGE_DURATION = 1000 * 60 * 10; // 10 minutes
const currentHost = window.location.hostname;
const sessionClosedKey = `TRUMPBONUSVARSLERLITE_CLOSED_${currentHost}`;
const messageShownKey = `TRUMPBONUSVARSLERLITE_MESSAGE_SHOWN_${currentHost}`;
// ---------------------------
// Early Exit Conditions
// ---------------------------
if (sessionStorage.getItem(sessionClosedKey) === "true") return;
const messageShownTimestamp = localStorage.getItem(messageShownKey);
if (messageShownTimestamp && (Date.now() - parseInt(messageShownTimestamp, 10)) < MESSAGE_DURATION) {
return;
}
// ---------------------------
// Main Entry: Fetch the Feed
// ---------------------------
function fetchFeed(attemptsLeft = 5) {
let feedData = null;
const storedTime = localStorage.getItem(LOCAL_KEY_TIME);
// Use cached data if it’s fresh
if (storedTime && (Date.now() - parseInt(storedTime, 10)) < CACHE_DURATION) {
const rawData = localStorage.getItem(LOCAL_KEY_DATA);
if (rawData) {
try {
feedData = JSON.parse(rawData);
} catch(e) {
console.error("Failed to parse cached JSON data:", e);
}
}
}
if (feedData) {
processFeed(feedData);
} else {
GM.xmlHttpRequest({
method: "GET",
url: FEED_URL,
headers: { "Accept": "application/json", "Cache-Control": "no-cache" },
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
try {
const json = JSON.parse(response.responseText);
localStorage.setItem(LOCAL_KEY_DATA, response.responseText);
localStorage.setItem(LOCAL_KEY_TIME, Date.now().toString());
processFeed(json);
} catch(e) {
console.error("Failed to parse fetched JSON data:", e);
retryOrFallback(attemptsLeft);
}
} else {
console.error(`Failed to fetch JSON data. Status: ${response.status}`);
retryOrFallback(attemptsLeft);
}
},
onerror: (error) => {
console.error("Error fetching JSON data:", error);
retryOrFallback(attemptsLeft);
}
});
}
}
function retryOrFallback(attemptsLeft) {
if (attemptsLeft > 1) {
console.warn(`Retrying primary feed... Attempts left: ${attemptsLeft - 1}`);
fetchFeed(attemptsLeft - 1);
} else {
console.warn("Primary feed failed after 5 tries. Using backup...");
fetchBackupFeed();
}
}
function fetchBackupFeed() {
GM.xmlHttpRequest({
method: "GET",
url: FEED_FALLBACK_URL,
headers: { "Accept": "application/json", "Cache-Control": "no-cache" },
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
try {
const json = JSON.parse(response.responseText);
localStorage.setItem(LOCAL_KEY_DATA, response.responseText);
processFeed(json);
} catch(e) {
console.error("Failed to parse backup JSON data:", e);
}
} else {
console.error(`Failed to fetch backup JSON data. Status: ${response.status}`);
}
},
onerror: (error) => {
console.error("Error fetching backup JSON data:", error);
}
});
}
// ---------------------------
// Match the host, ignoring "www."
// ---------------------------
function processFeed(json) {
if (!json || !json.merchants) return;
// 1) Exact match
let merchant = json.merchants[currentHost];
// 2) Try removing "www." if not found
if (!merchant) {
const noWww = currentHost.replace(/^www\./, '');
merchant = json.merchants[noWww];
}
// 3) If still not found, try adding "www." if original didn’t have it
if (!merchant && !/^www\./.test(currentHost)) {
merchant = json.merchants["www." + currentHost];
}
if (merchant) {
injectShadowNotifier(merchant);
}
}
// ---------------------------
// Adblock references
// ---------------------------
let adblockButtonRef = null; // store reference for the original button
let shadowRootRef = null; // might help if we need the parent of button
// ---------------------------
// Shadow DOM Notifier
// ---------------------------
function injectShadowNotifier(merchant) {
const shadowHost = document.createElement('div');
document.body.appendChild(shadowHost);
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
shadowRootRef = shadowRoot;
const styles = `
/* Reset + Bolder Design + link cursor */
:host, :host * {
all: unset;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
:host {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 15px;
line-height: 1.6;
color: #333;
}
:host a {
cursor: pointer; /* link cursor on anchors */
}
/* Container for the entire notification */
.notification-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 999999;
width: 360px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
overflow: hidden;
animation: slideIn 0.5s ease-out;
transition: width 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Wrapper for the notifier content */
.notification-wrapper {
display: flex;
flex-direction: column;
position: relative;
background-color: #fff;
}
/* Header section */
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f3f3f3;
border-bottom: 1px solid #ececec;
}
.notification-logo img {
max-height: 28px;
}
.notification-close-button img {
width: 22px;
height: 22px;
cursor: pointer;
transition: transform 0.2s;
}
.notification-close-button img:hover {
transform: scale(1.15);
}
/* Body section */
.notification-body {
padding: 16px;
background-color: #ffffff;
}
/* Text content */
.notification-text {
margin-bottom: 16px;
}
.notification-subtitle {
display: block;
font-size: 16px;
font-weight: 600;
margin-bottom: 10px;
}
.notification-cashback {
font-weight: 700;
font-size: 20px;
color: #4D4DFF;
display: block;
margin-bottom: 6px;
}
/* "Husk å" + list */
.husk-line {
margin: 0 0 6px 0;
font-weight: 500;
}
.notification-list {
list-style-type: decimal;
margin: 8px 0 0 20px;
padding: 0;
font-size: 13px;
display: block;
}
.notification-list li {
margin: 6px 0;
display: list-item;
}
/* Action button */
.notification-button {
display: block;
margin: 16px auto 0 auto;
padding: 12px 24px;
background: #4D4DFF;
color: #ffffff;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
text-align: center;
transition: background 0.3s;
cursor: pointer;
}
.notification-button:hover {
background: #3232ff;
}
/* Info button styling */
.notification-info-button {
position: absolute;
bottom: 8px;
right: 8px;
width: 16px;
height: 16px;
font-size: 9px;
font-weight: bold;
font-family: sans-serif;
color: #333;
background-color: #ccc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
opacity: 0.2;
transition: opacity 0.2s, background-color 0.2s;
cursor: pointer;
}
.notification-info-button:hover {
opacity: 0.45;
background-color: #bbb;
}
/* Hide the list on narrow screens */
@media (max-width: 700px) {
.notification-list {
display: none;
}
}
/* Adblock-detected styles: blood-red color & "annoying" animation */
.adblock-detected {
background: #ff0000 !important;
animation: adblockPulse 0.7s infinite alternate ease-in-out;
}
@keyframes adblockPulse {
from {
transform: scale(1);
}
to {
transform: scale(1.05);
}
}
`;
const styleEl = document.createElement('style');
styleEl.textContent = styles;
shadowRoot.appendChild(styleEl);
const container = document.createElement('div');
container.classList.add('notification-container');
container.innerHTML = `
<div class="notification-wrapper">
<div class="notification-header">
<div class="notification-logo">
<img src="https://trumfnetthandel.no/dest/img/Trumf/notifier/nett-handel-wrapper-logo.svg" alt="Trumf Nettbutikk Logo">
</div>
<div class="notification-close-button">
<img src="https://trumfnetthandel.no/dest/img/Trumf/notifier/close-button-wrapper.png" alt="Close">
</div>
</div>
<div class="notification-body">
<div class="notification-content">
<div class="notification-text">
<span class="notification-cashback">${merchant.cashbackDescription}</span>
<span class="notification-subtitle">Trumf-bonus hos ${merchant.name}</span>
<p class="husk-line">Husk å:</p>
<ol class="notification-list">
<li>Deaktivere uBlock Origin</li>
<li>Deaktivere AdGuard Home/Pi-Hole</li>
<li>Tømme handlevognen</li>
</ol>
</div>
<a class="notification-button"
href="https://trumfnetthandel.no/cashback/${merchant.urlName}"
target="_blank" rel="noopener noreferrer">
Få Trumf-bonus
</a>
</div>
</div>
</div>
`;
shadowRoot.appendChild(container);
// Info button
const infoButton = document.createElement('a');
infoButton.href = "https://github.com/NewsGuyTor/Trumf-Bonusvarsler-Lite";
infoButton.target = "_blank";
infoButton.rel = "noopener noreferrer";
infoButton.textContent = "i";
infoButton.classList.add("notification-info-button");
container.querySelector(".notification-wrapper").appendChild(infoButton);
// Close button
const closeBtn = container.querySelector(".notification-close-button img");
closeBtn?.addEventListener("click", () => {
sessionStorage.setItem(sessionClosedKey, "true");
document.body.removeChild(shadowHost);
});
// Action button
const button = container.querySelector(".notification-button");
adblockButtonRef = button; // store reference for adblock changes
button?.addEventListener("click", () => {
const bodyContent = container.querySelector(".notification-content");
if (bodyContent) {
bodyContent.innerHTML = `
<div class="notification-text">
Hvis alt ble gjort riktig, skal kjøpet ha blitt registrert.
</div>
`;
}
localStorage.setItem(messageShownKey, Date.now().toString());
});
// Button text adapt on small screens
const basicRate = merchant.basicRate;
function adjustNotifierUI() {
const windowWidth = window.innerWidth;
if (!button) return;
if (windowWidth <= WIDTH_THRESHOLD) {
if (!button.dataset.originalText) {
button.dataset.originalText = button.textContent.trim();
button.textContent = `Få ${basicRate} Trumf-bonus`;
}
} else {
if (button.dataset.originalText) {
button.textContent = button.dataset.originalText;
delete button.dataset.originalText;
}
}
}
adjustNotifierUI();
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(adjustNotifierUI, 150);
});
// Finally, check Adblock & transform button if detected
runAdblockDetection();
}
// ---------------------------
// Adblock detection code
// ---------------------------
const outbrainErrorCheck = async () => {
try {
const resp = await fetch("https://widgets.outbrain.com/outbrain.js");
await resp.text();
return false;
} catch (e) {
return true;
}
};
const adligatureErrorCheck = async () => {
try {
const resp = await fetch("https://adligature.com/", { mode: "no-cors" });
await resp.text();
return false;
} catch (e) {
return true;
}
};
const quantserveErrorCheck = async () => {
try {
const resp = await fetch("https://secure.quantserve.com/quant.js", { mode: "no-cors" });
await resp.text();
return false;
} catch (e) {
return true;
}
};
const adligatureCssErrorCheck = async () => {
try {
const resp = await fetch("https://cdn.adligature.com/work.ink/prod/rules.css", { mode: "no-cors" });
await resp.text();
return false;
} catch (e) {
return true;
}
};
const srvtrackErrorCheck = async () => {
try {
const resp = await fetch("https://srvtrck.com/assets/css/LineIcons.css", { mode: "no-cors" });
await resp.text();
return false;
} catch (e) {
return true;
}
};
const yieldkitCheck = async () => {
try {
const resp = await fetch("https://js.srvtrck.com/v1/js?api_key=40710abb89ad9e06874a667b2bc7dee7&site_id=1f10f78243674fcdba586e526cb8ef08", { mode: "no-cors" });
await resp.text();
return false;
} catch (e) {
return true;
}
};
const setIntervalCheck = () => {
return new Promise((resolve) => {
const timeout = setTimeout(() => {
resolve(true);
}, 2000);
const interval = setInterval(() => {
const a0b = "a0b";
if (a0b === "a0b") {
clearInterval(interval);
clearTimeout(timeout);
resolve(false);
}
}, 100);
});
};
// Hidden banners check
const idCheck = async () => {
const bannerIds = ["AdHeader", "AdContainer", "AD_Top", "homead", "ad-lead"];
const bannerString = bannerIds.map((bannerId) => `<div id="${bannerId}"> </div>`).join('');
const dataContainer = document.createElement("div");
dataContainer.innerHTML = bannerString;
document.body.append(dataContainer);
let adblocker = false;
bannerIds.forEach(id => {
const elem = document.getElementById(id);
if (!elem || elem.offsetHeight === 0) {
adblocker = true;
}
elem?.remove();
});
return adblocker;
};
const detectedAdblock = async () => {
const resp = await Promise.all([
outbrainErrorCheck(),
adligatureErrorCheck(),
quantserveErrorCheck(),
adligatureCssErrorCheck(),
srvtrackErrorCheck(),
setIntervalCheck(),
yieldkitCheck()
]);
const isNotUsingAdblocker = resp.every(r => r === false);
const bannerBlocked = await idCheck(); // additional check
return !isNotUsingAdblocker || bannerBlocked;
};
async function runAdblockDetection() {
const isAdblock = await detectedAdblock();
if (isAdblock && adblockButtonRef) {
// Create a fresh button to replace original (removing old click handler)
const newButton = adblockButtonRef.cloneNode(true);
adblockButtonRef.parentNode.replaceChild(newButton, adblockButtonRef);
newButton.classList.add("adblock-detected");
newButton.textContent = "Adblocker funnet!";
// remove link/href
newButton.removeAttribute("href");
newButton.removeAttribute("target");
// On click -> reload the site
newButton.addEventListener("click", (evt) => {
evt.preventDefault();
location.reload();
});
}
}
// ---------------------------
// Initialize
// ---------------------------
fetchFeed();
})();