Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# build output
dist/

# misc local files
local/

# generated types
.astro/

Expand Down
88 changes: 81 additions & 7 deletions src/assets/data/promotions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
export type PromoType = "banner" | "video";
import audioComPromoImage from "../img/promo/audacity-audiocom-promo.png";

export type PromoType = "banner" | "video" | "exit-popup";

export type ExitPopupPolicy = {
sessionCap?: number;
dismissCooldownDays?: number;
minDwellMs?: number;
};

export type ExitPopupOptions = {
routeAllowlist: string[];
displayMode?: "toast" | "modal";
promoImageSrc?: string;
promoImageAlt?: string;
title: string;
body?: string;
dismissText: string;
policy?: ExitPopupPolicy;
impressionTracking?: TrackingConfig;
dismissTracking?: TrackingConfig;
};

export type TrackingConfig = {
category: string;
action: string;
name: string;
};

export type PromoData = {
type?: PromoType;
type: PromoType;
isActive?: boolean;
priority?: number;
slot?: number;
Expand All @@ -13,15 +40,12 @@ export type PromoData = {
message?: string;
button?: string;
};
tracking?: {
category: string;
action: string;
name: string;
};
tracking?: TrackingConfig;
cta?: {
text: string;
link: string;
};
popupOptions?: ExitPopupOptions;
// Video-specific properties
video?: {
placeholderImage: string;
Expand All @@ -36,6 +60,9 @@ type FilterOptions = {
path?: string | null;
};

const routeMatchesAllowlist = (path: string, allowlist: string[]) =>
allowlist.some((route) => path === route || path.startsWith(`${route}/`));

/** Get all promos matching the filter criteria */
export const getFilteredPromos = (
promos: PromoData[],
Expand All @@ -50,6 +77,11 @@ export const getFilteredPromos = (
// Check type match
if (type && promo.type !== type) return false;

if (path && promo.type === "exit-popup") {
const allowlist = promo.popupOptions?.routeAllowlist ?? [];
if (!routeMatchesAllowlist(path, allowlist)) return false;
}

// Check path suppression
if (path && promo.suppressOnPaths?.includes(path)) return false;

Expand All @@ -62,6 +94,11 @@ export const getFilteredPromos = (
});
};

const AUDIO_COM_EXIT_POPUP_IMAGE_SRC =
typeof audioComPromoImage === "string"
? audioComPromoImage
: audioComPromoImage.src;

const promoData: Record<string, PromoData> = {
// === BANNER PROMOS ===
audacity4Alpha: {
Expand Down Expand Up @@ -280,6 +317,43 @@ const promoData: Record<string, PromoData> = {
videoURL: "https://www.youtube-nocookie.com/embed/A4jPvCdbrKA?autoplay=1",
},
},
audioComExitPopup: {
type: "exit-popup",
isActive: true,
priority: 50,
message:
"Use Audio.com to back up your projects, and share them from anywhere!",
cta: {
text: "Join Audio.com",
link: "https://audio.com/",
},
popupOptions: {
title: "Keep your audio safe in the cloud",
routeAllowlist: ["/download", "/post-download", "/cloud-saving"],
displayMode: "modal",
promoImageSrc: AUDIO_COM_EXIT_POPUP_IMAGE_SRC,
promoImageAlt: "Audio.com promotion",
dismissText: "Not now",
policy: {
minDwellMs: 3000,
},
impressionTracking: {
category: "Exit Intent",
action: "exit_intent_impression",
name: "audio.com Exit Intent Popup",
},
dismissTracking: {
category: "Exit Intent",
action: "exit_intent_dismiss",
name: "audio.com Exit Intent Popup",
},
},
tracking: {
category: "Exit Intent",
action: "exit_intent_cta_click",
name: "audio.com Exit Intent Popup",
},
},
};

export default promoData;
52 changes: 52 additions & 0 deletions src/assets/img/promo/audio-com-exit-intent-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
162 changes: 159 additions & 3 deletions src/assets/js/cookieConsent.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,91 @@ const cookieStorage = {
},
setItem: (key, value) => {
document.cookie = `${key}=${value}; expires=${new Date(
new Date().getTime() + 1000 * 60 * 60 * 24 * 365
new Date().getTime() + 1000 * 60 * 60 * 24 * 365,
).toGMTString()}; path=/ `;
},
};

const storageType = cookieStorage;
const consentPropertyName = "audacity_consent";
const ATTENTION_OVERLAY_CHANNEL = "attention-overlay";
const COOKIE_CONSENT_OWNER = "cookie-consent";
const COOKIE_CONSENT_PRIORITY = 10;

const getUiSemaphore = () => {
if (typeof window === "undefined") {
return null;
}

if (window.__audacityUiSemaphore) {
return window.__audacityUiSemaphore;
}

const state = window.__audacityUiSemaphoreState || {
locks: new Map(),
listeners: new Set(),
};

window.__audacityUiSemaphoreState = state;

const notify = (channel) => {
const lock = state.locks.get(channel) || null;
state.listeners.forEach((listener) => listener(channel, lock));
};

const semaphore = {
acquire(channel, owner, options) {
const requestedPriority = options?.priority || 0;
const shouldPreempt = options?.preempt || false;
const currentLock = state.locks.get(channel);

if (currentLock && currentLock.owner !== owner) {
if (!(shouldPreempt && requestedPriority > currentLock.priority)) {
return false;
}
}

if (
currentLock &&
currentLock.owner === owner &&
currentLock.priority === requestedPriority
) {
return true;
}

state.locks.set(channel, {
owner,
priority: requestedPriority,
});
notify(channel);
return true;
},
release(channel, owner) {
const currentLock = state.locks.get(channel);
if (!currentLock || currentLock.owner !== owner) {
return false;
}

state.locks.delete(channel);
notify(channel);
},
isLocked(channel) {
return state.locks.has(channel);
},
getLock(channel) {
return state.locks.get(channel) || null;
},
subscribe(listener) {
state.listeners.add(listener);
return () => {
state.listeners.delete(listener);
};
},
};

window.__audacityUiSemaphore = semaphore;
return semaphore;
};

const showShowPopup = () => !storageType.getItem(consentPropertyName);
const saveAcceptToStorage = () =>
Expand All @@ -26,12 +104,46 @@ window.addEventListener("load", function () {
const consentPopup = document.getElementById("consent-popup");
const acceptBtn = document.getElementById("accept");
const rejectBtn = document.getElementById("reject");
const semaphore = getUiSemaphore();
let popupRetryTimeoutId = null;
let shouldAttemptPopup = true;
let tryShowPopup = () => {};

const releaseOverlayLock = () => {
if (!semaphore) {
return;
}

semaphore.release(ATTENTION_OVERLAY_CHANNEL, COOKIE_CONSENT_OWNER);
};

const stopPopupAttempts = () => {
shouldAttemptPopup = false;
if (popupRetryTimeoutId !== null) {
window.clearTimeout(popupRetryTimeoutId);
popupRetryTimeoutId = null;
}
};

const queuePopupRetry = () => {
if (!shouldAttemptPopup || !showShowPopup(storageType)) {
return;
}

if (popupRetryTimeoutId !== null) {
window.clearTimeout(popupRetryTimeoutId);
}

popupRetryTimeoutId = window.setTimeout(tryShowPopup, 300);
};

const acceptCookie = (event) => {
event.preventDefault();

saveAcceptToStorage(storageType);
stopPopupAttempts();
consentPopup.classList.add("hide");
releaseOverlayLock();
if (typeof _paq !== "undefined") {
_paq.push(["setCookieConsentGiven"]);
}
Expand All @@ -41,15 +153,59 @@ window.addEventListener("load", function () {
event.preventDefault();

saveRejectToStorage(storageType);
stopPopupAttempts();
consentPopup.classList.add("hide");
releaseOverlayLock();
};

acceptBtn.addEventListener("click", acceptCookie);
rejectBtn.addEventListener("click", rejectCookie);

if (semaphore) {
semaphore.subscribe((channel, lock) => {
if (channel !== ATTENTION_OVERLAY_CHANNEL || !consentPopup) {
return;
}

const ownedByCookie = lock?.owner === COOKIE_CONSENT_OWNER;
if (!ownedByCookie && !consentPopup.classList.contains("hide")) {
consentPopup.classList.add("hide");
}

if (!lock && shouldAttemptPopup && showShowPopup(storageType)) {
queuePopupRetry();
}
});
}

if (showShowPopup(storageType)) {
setTimeout(() => {
tryShowPopup = () => {
if (!shouldAttemptPopup || !showShowPopup(storageType)) {
return;
}

if (!semaphore) {
consentPopup.classList.remove("hide");
return;
}

const acquired = semaphore.acquire(
ATTENTION_OVERLAY_CHANNEL,
COOKIE_CONSENT_OWNER,
{
priority: COOKIE_CONSENT_PRIORITY,
preempt: false,
},
);

if (!acquired) {
queuePopupRetry();
return;
}

consentPopup.classList.remove("hide");
}, 2000);
};

setTimeout(tryShowPopup, 2000);
}
});
Loading