mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
83 lines
3.5 KiB
TypeScript
83 lines
3.5 KiB
TypeScript
/**
|
|
* The campaign a visitor arrived from: the link's utm_* tags and the ad
|
|
* networks' click ids — the keys pay carries on every hop inside it
|
|
* (hanzo-inc/pay lib/campaign). A page that sends a visitor on to pay hands
|
|
* them over as they arrived, so a purchase is attributed to the link that
|
|
* brought the buyer. Read off the address, or off what this tab kept from the
|
|
* page the visit landed on, never invented.
|
|
*/
|
|
|
|
import { CONSENT_EVENT, declined, readConsent, storedConsent, type Choice } from '@hanzo/event'
|
|
|
|
/** The query keys a campaign or referral/affiliate link is carried in. */
|
|
const KEY = /^(utm_(source|medium|campaign|term|content|id)|gclid|gbraid|wbraid|fbclid|msclkid|ttclid|li_fat_id|ref|aff)$/
|
|
|
|
/** At most this many keys, each value at most this long: pay's own bounds. */
|
|
const MOST = 12
|
|
const LONGEST = 200
|
|
|
|
/** The campaign keys in a query, in the order they came, and nothing else. */
|
|
export function campaign(search: string): [string, string][] {
|
|
const out: [string, string][] = []
|
|
for (const [k, v] of new URLSearchParams(search)) {
|
|
if (out.length === MOST) break
|
|
if (KEY.test(k) && v) out.push([k, v.slice(0, LONGEST)])
|
|
}
|
|
return out
|
|
}
|
|
|
|
/** Where this tab keeps the campaign its visit landed with: one tab, one visit. */
|
|
const KEPT = 'hz_campaign'
|
|
|
|
/**
|
|
* The keys a choice lets this tab keep: a utm/ref/aff tag under Analytics and an ad
|
|
* network's click id under Marketing, defaulting to opt-in unless declined or explicitly turned off.
|
|
*/
|
|
function allowed(keys: [string, string][], c: Choice): [string, string][] {
|
|
const hasExplicit = storedConsent()
|
|
const isDeclined = declined()
|
|
const allowAnalytics = isDeclined ? false : hasExplicit ? c.analytics : true
|
|
const allowMarketing = isDeclined ? false : hasExplicit ? c.marketing : true
|
|
return keys.filter(([k]) => (k.startsWith('utm_') || k === 'ref' || k === 'aff' ? allowAnalytics : allowMarketing))
|
|
}
|
|
|
|
/** Holds the kept campaign to a choice: the landing's own keys, else the ones kept before, as far as it allows. */
|
|
function hold(search: string, c: Choice): void {
|
|
try {
|
|
const store = window.sessionStorage
|
|
const here = campaign(search)
|
|
const keep = allowed(here.length ? here : campaign(store.getItem(KEPT) ?? ''), c)
|
|
if (keep.length) store.setItem(KEPT, new URLSearchParams(keep).toString())
|
|
else store.removeItem(KEPT)
|
|
} catch {
|
|
/* no store: the address alone carries them */
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Keep the campaign the address carries for this tab, as far as the visitor's
|
|
* choice allows, and hold it to that choice for the page's life: the page a
|
|
* visitor lands on calls it (app/providers.tsx), so the tags outlive a sign-in
|
|
* that takes them out of the address. Nothing is kept before a choice; an Accept
|
|
* on this page keeps what it now allows, and a withdrawal drops what it no longer
|
|
* does, here and on every later page. A later page with tags of its own replaces
|
|
* them; one with none leaves them. Returns the function that stops listening.
|
|
*/
|
|
export function arrive(search: string): () => void {
|
|
const keep = () => hold(search, readConsent())
|
|
keep()
|
|
window.addEventListener(CONSENT_EVENT, keep)
|
|
return () => window.removeEventListener(CONSENT_EVENT, keep)
|
|
}
|
|
|
|
/** The campaign that brought this visit: the address's own keys, else the ones this tab kept from the page it landed on. */
|
|
export function brought(search: string): [string, string][] {
|
|
const here = campaign(search)
|
|
if (here.length) return here
|
|
try {
|
|
return campaign(window.sessionStorage.getItem(KEPT) ?? '')
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|