|
export enum Consent { |
|
UNDECIDED, |
|
YES, |
|
NO, |
|
} |
|
|
|
export type Asker = (consent: Map<string, Consent>, done: () => void) => void; |
|
|
|
const STORAGE_KEY = 'mrhenry/cookie-consent'; |
|
|
|
export class CookieConsent { |
|
private spec: Set<string>; |
|
private consent: Map<string, Consent>; |
|
private asker: Asker; |
|
private asking = false; |
|
private pending: Array<[string, (consent: Consent) => void]> = []; |
|
|
|
constructor(sections: string[], asker: Asker) { |
|
this.spec = new Set(sections); |
|
this.consent = new Map(); |
|
|
|
const value = window.localStorage.getItem(STORAGE_KEY) || '{}'; |
|
const stored: {[key:string]:bool} = JSON.parse(value); |
|
for (let section of this.spec) { |
|
let consent = stored[section]; |
|
if (consent === Consent.YES || value === Consent.NO) { |
|
this.consent.set(section, consent); |
|
} else { |
|
this.consent.set(section, Consent.UNDECIDED); |
|
} |
|
} |
|
} |
|
|
|
private ask() { |
|
if (this.asking) { |
|
return; |
|
} |
|
|
|
// clone old consent |
|
const prev = new Map(this.consent); |
|
|
|
// ask for consent |
|
this.asking = true; |
|
this.asker(prev, () => { |
|
let pending = this.pending; |
|
this.asking = false; |
|
this.pending = []; |
|
|
|
// capture new consent |
|
const next = new Map<string, Consent>(); |
|
for (let section of this.spec) { |
|
let consent = prev.get(section); |
|
if (consent === Consent.YES || value === Consent.NO) { |
|
next.set(section, consent); |
|
} else { |
|
next.set(section, Consent.UNDECIDED); |
|
} |
|
} |
|
|
|
// store and set new consent; |
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); |
|
this.consent = next; |
|
|
|
for (let [section, resolve] of pending) { |
|
resolve(next.get(section) || Consent.UNDECIDED); |
|
} |
|
}); |
|
} |
|
|
|
private askOnce(section: string): Promise<Consent> { |
|
const p = new Promise(resolve => { |
|
if (!this.spec.has(section)) { |
|
console.error('Consent section '+JSON.stringify(section)+' was not registered! (defaulting to NO)'); |
|
resolve(Consent.NO); |
|
return; |
|
} |
|
this.pending.push([section, resolve]); |
|
this.ask(); |
|
}); |
|
} |
|
|
|
async getConsent(section: string) : Promise<bool> { |
|
while (true) { |
|
let consent = await this.askOnce(section); |
|
if (consent===Consent.UNDECIDED) { |
|
continue; |
|
} |
|
return consent === Consent.YES; |
|
} |
|
} |
|
} |
|
|
|
const EXAMPLE = new CookieConsent( |
|
['tracking', 'marketing'], |
|
(consent: Map<string, Consent>, done: () => void) => { |
|
// update the consent Map as requested by the user. |
|
// call done() to store/update consent. |
|
} |
|
) |
|
|
|
if (await EXAMPLE.getConsent('tracking')) { |
|
// we have tracking consent |
|
// ... |
|
} |