11/**
22 * Cookie Consent Manager for Essential C#
33 * Implements Google Consent Mode v2 for Microsoft Clarity and Google Analytics
4- * Compliant with GDPR requirements for EEA, UK, and Switzerland
4+ * Shown to all visitors — ensures GDPR compliance globally without fragile region detection.
55 */
66
77class ConsentManager {
88 constructor ( options = { } ) {
99 this . COOKIE_NAME = 'essential-csharp-consent' ;
1010 this . COOKIE_DURATION = 365 ; // days
11+ this . CONSENT_VERSION = '2' ; // Bump this to re-prompt all users when consent terms change
1112 this . GOOGLE_ANALYTICS_ID = options . googleAnalyticsId || 'G-761B4BMK2R' ;
1213 this . consentState = {
1314 analytics_storage : 'denied' ,
@@ -19,20 +20,20 @@ class ConsentManager {
1920 personalization_storage : 'denied'
2021 } ;
2122
22- // Check if user is in EEA/UK/Switzerland region
23- this . requiresConsent = this . checkRegionRequiresConsent ( ) ;
24-
2523 this . init ( ) ;
2624 }
2725
2826 init ( ) {
29- // Initialize Google Consent Mode
3027 this . initGoogleConsentMode ( ) ;
3128
32- // Load saved consent preferences
29+ // Load saved consent preferences (signals Clarity/GA if already consented)
3330 this . loadConsentPreferences ( ) ;
31+
32+ // Always send Clarity the current consent state (denied by default for new visitors).
33+ // Clarity's polyfill queues this call and delivers it when the script loads.
34+ this . updateClarityConsent ( ) ;
3435
35- // Show banner if consent required and not yet given
36+ // Show banner if no valid consent stored
3637 if ( this . shouldShowConsentBanner ( ) ) {
3738 this . showConsentBanner ( ) ;
3839 }
@@ -42,77 +43,40 @@ class ConsentManager {
4243 }
4344
4445 dispatchInitializationEvent ( ) {
45- // Create and dispatch custom event to signal consent manager is ready
4646 const event = new CustomEvent ( 'consentManagerReady' , {
4747 detail : {
4848 hasAnalyticsConsent : this . hasAnalyticsConsent ( ) ,
49- hasAdvertisingConsent : this . hasAdvertisingConsent ( ) ,
50- requiresConsent : this . requiresConsent
49+ hasAdvertisingConsent : this . hasAdvertisingConsent ( )
5150 }
5251 } ) ;
5352 document . dispatchEvent ( event ) ;
5453 }
5554
5655 initGoogleConsentMode ( ) {
57- // Initialize gtag if not already loaded
56+ // Ensure gtag infrastructure exists — the actual 'consent default' is set inline
57+ // in _Layout.cshtml before gtag.js loads (required by Google Consent Mode v2).
5858 window . dataLayer = window . dataLayer || [ ] ;
5959 function gtag ( ) { dataLayer . push ( arguments ) ; }
6060 window . gtag = window . gtag || gtag ;
61-
62- // Set default consent state - denial for all except essential
63- gtag ( 'consent' , 'default' , this . consentState ) ;
64- }
65-
66- checkRegionRequiresConsent ( ) {
67- // Check for forced testing via URL parameter
68- const urlParams = new URLSearchParams ( window . location . search ) ;
69- if ( urlParams . get ( 'testConsent' ) === 'true' ) {
70- return true ;
71- }
72-
73- // Timezone-based region detection for GDPR compliance
74- // Users can change timezones, but this provides reasonable detection for most cases
75- const timezone = Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone ;
76-
77- // EEA countries, UK, and Switzerland timezones
78- const requiresConsentTimezones = [
79- // Western Europe
80- 'Europe/London' , 'Europe/Dublin' , 'Europe/Lisbon' , 'Europe/Madrid' ,
81- 'Europe/Paris' , 'Europe/Amsterdam' , 'Europe/Brussels' , 'Europe/Luxembourg' ,
82- 'Europe/Zurich' , 'Europe/Vienna' , 'Europe/Rome' , 'Europe/Vatican' ,
83- 'Europe/San_Marino' , 'Europe/Malta' , 'Europe/Monaco' ,
84-
85- // Central Europe
86- 'Europe/Berlin' , 'Europe/Prague' , 'Europe/Budapest' , 'Europe/Warsaw' ,
87- 'Europe/Bratislava' , 'Europe/Ljubljana' , 'Europe/Zagreb' , 'Europe/Belgrade' ,
88- 'Europe/Sarajevo' , 'Europe/Podgorica' , 'Europe/Skopje' , 'Europe/Tirane' ,
89-
90- // Northern Europe
91- 'Europe/Stockholm' , 'Europe/Oslo' , 'Europe/Copenhagen' , 'Europe/Helsinki' ,
92- 'Europe/Tallinn' , 'Europe/Riga' , 'Europe/Vilnius' , 'Europe/Reykjavik' ,
93-
94- // Eastern Europe
95- 'Europe/Bucharest' , 'Europe/Sofia' , 'Europe/Athens' , 'Europe/Nicosia' ,
96-
97- // Additional EEA territories
98- 'Atlantic/Canary' , 'Atlantic/Madeira' , 'Atlantic/Azores' ,
99- 'Europe/Gibraltar' , 'Africa/Ceuta'
100- ] ;
101-
102- return requiresConsentTimezones . includes ( timezone ) ;
10361 }
10462
10563 loadConsentPreferences ( ) {
10664 const saved = this . getCookie ( this . COOKIE_NAME ) ;
10765 if ( saved ) {
10866 try {
10967 const preferences = JSON . parse ( saved ) ;
68+
69+ // Invalidate stale consent if the version has changed — treat user as new visitor
70+ if ( preferences . _version !== this . CONSENT_VERSION ) {
71+ this . deleteCookie ( this . COOKIE_NAME ) ;
72+ return ;
73+ }
11074
111- // Validate and only apply known consent properties for security
75+ // Validate and only apply known consent properties for security.
76+ // Exclude functionality_storage and security_storage — always essential, never user-overrideable.
11277 const validConsentKeys = [
11378 'analytics_storage' , 'ad_storage' , 'ad_user_data' ,
114- 'ad_personalization' , 'functionality_storage' ,
115- 'security_storage' , 'personalization_storage'
79+ 'ad_personalization' , 'personalization_storage'
11680 ] ;
11781
11882 const validatedPreferences = { } ;
@@ -132,8 +96,11 @@ class ConsentManager {
13296 }
13397
13498 shouldShowConsentBanner ( ) {
135- // Show banner if in EEA/UK/Switzerland and no consent stored
136- return this . requiresConsent && ! this . getCookie ( this . COOKIE_NAME ) ;
99+ // Allow forcing the banner via URL param (useful for testing)
100+ const urlParams = new URLSearchParams ( window . location . search ) ;
101+ if ( urlParams . get ( 'testConsent' ) === 'true' ) return true ;
102+ // Show to all visitors who haven't given valid consent yet
103+ return ! this . getCookie ( this . COOKIE_NAME ) ;
137104 }
138105
139106 showConsentBanner ( ) {
@@ -154,7 +121,7 @@ class ConsentManager {
154121 <div class="consent-banner-content">
155122 <div class="consent-banner-text">
156123 <h3>Cookie Preferences</h3>
157- <p>We use cookies to improve your experience and analyze website usage. You can manage your preferences below .</p>
124+ <p>We use cookies to improve your experience and analyze website usage. See our <a href="https://intellitect.com/about/privacy-policy/" target="_blank" rel="noopener noreferrer">Privacy Policy</a> for details .</p>
158125 </div>
159126 <div class="consent-banner-actions">
160127 <button id="consent-reject-all" class="btn btn-outline-secondary me-2">Reject All</button>
@@ -272,15 +239,20 @@ class ConsentManager {
272239 ad_storage : advertisingChecked ? 'granted' : 'denied' ,
273240 ad_user_data : advertisingChecked ? 'granted' : 'denied' ,
274241 ad_personalization : advertisingChecked ? 'granted' : 'denied' ,
275- personalization_storage : analyticsChecked ? 'granted' : 'denied'
242+ personalization_storage : advertisingChecked ? 'granted' : 'denied'
276243 } ;
277244
278245 this . saveConsentAndClose ( ) ;
279246 }
280247
281248 saveConsentAndClose ( ) {
282- // Save consent to cookie
283- this . setCookie ( this . COOKIE_NAME , JSON . stringify ( this . consentState ) , this . COOKIE_DURATION ) ;
249+ // Save consent with audit metadata
250+ const payload = {
251+ ...this . consentState ,
252+ _timestamp : new Date ( ) . toISOString ( ) ,
253+ _version : this . CONSENT_VERSION
254+ } ;
255+ this . setCookie ( this . COOKIE_NAME , JSON . stringify ( payload ) , this . COOKIE_DURATION ) ;
284256
285257 // Update consent mode
286258 this . updateConsentMode ( ) ;
@@ -290,17 +262,20 @@ class ConsentManager {
290262
291263 // Remove banner
292264 this . removeConsentBanner ( ) ;
265+
266+ // Notify layout scripts so they can fire gtag('config') on interactive consent
267+ document . dispatchEvent ( new CustomEvent ( 'consentUpdated' , {
268+ detail : {
269+ hasAnalyticsConsent : this . hasAnalyticsConsent ( ) ,
270+ hasAdvertisingConsent : this . hasAdvertisingConsent ( )
271+ }
272+ } ) ) ;
293273 }
294274
295275 updateConsentMode ( ) {
296276 if ( window . gtag ) {
297277 try {
298278 gtag ( 'consent' , 'update' , this . consentState ) ;
299-
300- // Configure Google Analytics if analytics consent is granted
301- if ( this . consentState . analytics_storage === 'granted' ) {
302- gtag ( 'config' , 'G-761B4BMK2R' ) ;
303- }
304279 } catch ( error ) {
305280 console . warn ( 'Failed to update Google Consent Mode:' , error ) ;
306281 }
@@ -439,7 +414,13 @@ class ConsentManager {
439414 setCookie ( name , value , days ) {
440415 const expires = new Date ( ) ;
441416 expires . setTime ( expires . getTime ( ) + ( days * 24 * 60 * 60 * 1000 ) ) ;
442- document . cookie = `${ name } =${ value } ;expires=${ expires . toUTCString ( ) } ;path=/;SameSite=Lax` ;
417+ const secure = window . location . protocol === 'https:' ? ';Secure' : '' ;
418+ document . cookie = `${ name } =${ value } ;expires=${ expires . toUTCString ( ) } ;path=/;SameSite=Lax${ secure } ` ;
419+ }
420+
421+ deleteCookie ( name ) {
422+ const secure = window . location . protocol === 'https:' ? ';Secure' : '' ;
423+ document . cookie = `${ name } =;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;SameSite=Lax${ secure } ` ;
443424 }
444425
445426 getCookie ( name ) {
@@ -456,12 +437,7 @@ class ConsentManager {
456437 // Public API for consent preference management
457438 openConsentPreferences ( ) {
458439 this . showConsentBanner ( ) ;
459- // If banner already exists, show customize options
460- setTimeout ( ( ) => {
461- if ( document . getElementById ( 'consent-banner' ) ) {
462- this . showCustomizeOptions ( ) ;
463- }
464- } , 100 ) ;
440+ this . showCustomizeOptions ( ) ;
465441 }
466442
467443 // Check current consent status
@@ -478,17 +454,21 @@ class ConsentManager {
478454 this . rejectAllConsent ( ) ;
479455 // Also clear any existing tracking cookies
480456 this . clearTrackingCookies ( ) ;
481- // Erase Clarity cookies according to documentation
482- if ( window . clarity ) {
483- clarity ( 'consent' , false ) ;
484- }
457+ // Note: Clarity consent signal is sent via updateClarityConsent() inside rejectAllConsent() → saveConsentAndClose()
485458 }
486459
487460 clearTrackingCookies ( ) {
488461 // Clear common tracking cookies (Google Analytics and Microsoft Clarity)
489462 const trackingCookies = [ '_ga' , '_gid' , '_gat' , '_clck' , '_clsk' , 'CLID' , 'ANONCHK' , 'MR' , 'MUID' , 'SM' ] ;
463+ const expired = 'expires=Thu, 01 Jan 1970 00:00:00 GMT' ;
464+ // GA and Clarity cookies are often set on the root domain, so attempt deletion on both the
465+ // exact hostname and the parent domain (e.g. .essentialcsharp.com)
466+ const hostname = window . location . hostname ;
467+ const rootDomain = '.' + hostname . split ( '.' ) . slice ( - 2 ) . join ( '.' ) ;
490468 trackingCookies . forEach ( cookieName => {
491- document . cookie = `${ cookieName } =;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/` ;
469+ document . cookie = `${ cookieName } =;${ expired } ;path=/` ;
470+ document . cookie = `${ cookieName } =;${ expired } ;path=/;domain=${ hostname } ` ;
471+ document . cookie = `${ cookieName } =;${ expired } ;path=/;domain=${ rootDomain } ` ;
492472 } ) ;
493473 }
494474}
0 commit comments