fivo_cookie_consent 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,524 @@
1
+ # fivo_cookie_consent
2
+
3
+ GDPR cookie-banner for Rails 7 (esbuild/jsbundling).
4
+
5
+ A minimal, opinionated GDPR cookie consent banner for Rails 7 projects using esbuild + jsbundling-rails + sass. Designed specifically for German-only audiences without I18n complexity.
6
+
7
+ ## Features
8
+
9
+ - 🚀 **Rails Engine** - Auto-mounted routes, views, and helpers
10
+ - 📱 **Responsive Design** - Mobile-first with desktop optimization
11
+ - 🎨 **Modern UI** - Clean design with WCAG-AA contrast compliance
12
+ - 🛠 **Configurable** - Customize labels, URLs, and cookie lifespan
13
+ - 📦 **localStorage** - Persistent consent storage with expiration
14
+ - 🎯 **Event-driven** - Custom events for analytics integration
15
+ - ♿ **Accessible** - Full keyboard navigation and screen reader support
16
+ - 🧪 **Well-tested** - Comprehensive RSpec + Capybara test suite
17
+ - 🔍 **Auto-Detection** - Automatically detects and categorizes cookies (client-side)
18
+ - 🛡️ **Server-Side Cookies** - Detects HttpOnly Rails session/CSRF cookies on the server and merges them client-side so users always see *all* cookies
19
+ - 📋 **Expandable Lists** - Collapsible cookie details with name, duration, and description
20
+ - 🚫 **Empty State Handling** - Graceful handling of categories with no cookies
21
+ - ⚙️ **Script Loaders** - Consent-gated loading for GTM, GA4, reCAPTCHA, Hotjar, and Facebook Pixel
22
+ - 🧩 **Extensible API** - Ruby API and JS facade for registering and loading custom third-party scripts
23
+
24
+ ## Installation
25
+
26
+ ### From RubyGems (when published)
27
+
28
+ Add this line to your application's Gemfile:
29
+
30
+ ```ruby
31
+ gem 'fivo_cookie_consent'
32
+ ```
33
+
34
+ And then execute:
35
+
36
+ ```bash
37
+ bundle add fivo_cookie_consent
38
+ ```
39
+
40
+ ### From GitLab Repository (current)
41
+
42
+ To use the latest version directly from GitLab, add this to your Gemfile:
43
+
44
+ ```ruby
45
+ # GDPR Cookie Consent
46
+ gem 'fivo_cookie_consent', git: 'https://gitlab.com/fivo/fivo_cookie_consent.git'
47
+ ```
48
+
49
+ Or specify a branch/tag:
50
+
51
+ ```ruby
52
+ # Use main branch (recommended)
53
+ gem 'fivo_cookie_consent', git: 'https://gitlab.com/fivo/fivo_cookie_consent.git', branch: 'main'
54
+
55
+ # Or use a specific tag
56
+ gem 'fivo_cookie_consent', git: 'https://gitlab.com/fivo/fivo_cookie_consent.git', tag: 'v0.1.0'
57
+ ```
58
+
59
+ Then execute:
60
+
61
+ ```bash
62
+ bundle install
63
+ ```
64
+
65
+ Run the generator to install required files:
66
+
67
+ ```bash
68
+ rails generate fivo_cookie_consent:install
69
+ ```
70
+
71
+ ### Manual Setup
72
+
73
+ After running the generator, add the imports to your bundler files:
74
+
75
+ **app/javascript/application.js**
76
+ ```javascript
77
+ import "./fivo_cookie_consent"
78
+ ```
79
+
80
+ **app/assets/stylesheets/application.scss**
81
+ ```scss
82
+ @use "fivo_cookie_consent";
83
+ ```
84
+
85
+ ## Usage
86
+
87
+ Add the cookie banner to your layout before the closing `</body>` tag:
88
+
89
+ ```erb
90
+ <!-- app/views/layouts/application.html.erb -->
91
+ <%= gdpr_cookie_banner %>
92
+ ```
93
+
94
+ The helper injects a `data-server-cookies` attribute containing a JSON string of HttpOnly cookies detected on the server (Rails session, CSRF token, etc.).
95
+
96
+ **Important:** Do **not** mark this JSON as `html_safe`. The gem’s helper already escapes it correctly—see `server_cookies_json`—so the string remains valid markup and can be parsed on the client with `JSON.parse()`.
97
+
98
+ The banner will automatically appear for new visitors and hide once consent is given.
99
+
100
+ ### Step 1: Add the banner and assets
101
+
102
+ 1. Ensure you have imported the JS and SCSS as shown in the "Manual Setup" section.
103
+ 2. Add `<%= gdpr_cookie_banner %>` once in your main layout, typically `app/views/layouts/application.html.erb`, just before `</body>`.
104
+
105
+ ### Step 2: Choose how to load analytics/marketing scripts
106
+
107
+ You have two primary integration modes:
108
+
109
+ 1. **Strict lazy-load** – third-party scripts are not loaded at all until the user has given consent for the relevant category.
110
+ 2. **Google Consent Mode pre-load** – GTM/GA4 are loaded immediately but with storage denied by default; consent updates are forwarded to Consent Mode.
111
+
112
+ #### Option A: Strict lazy-load for GTM/GA4
113
+
114
+ Add consent-gated placeholders to your `<head>` using `gdpr_lazy_load_tag`:
115
+
116
+ ```erb
117
+ <!-- app/views/layouts/application.html.erb -->
118
+ <head>
119
+ <title>My App</title>
120
+
121
+ <%# Strict lazy-load: scripts are only loaded after consent %>
122
+ <%= gdpr_lazy_load_tag(:gtm, id: 'GTM-XXXX') %>
123
+ <%= gdpr_lazy_load_tag(:ga4, id: 'G-XXXXXXX') %>
124
+
125
+ <%= csrf_meta_tags %>
126
+ <%= csp_meta_tag %>
127
+ </head>
128
+ ```
129
+
130
+ What this does:
131
+
132
+ - Renders `<script type="text/plain" data-gdpr-category="analytics" ...>` placeholders.
133
+ - The gem’s JavaScript turns these placeholders into real `<script>` tags **only if** the user has granted consent for the `analytics` category.
134
+ - Works with both "Accept all" and granular settings in the modal.
135
+
136
+ #### Option B: Google Consent Mode pre-load for GTM/GA4
137
+
138
+ If you want to use Google Consent Mode, call `gdpr_consent_mode_defaults` **before** your GTM/GA scripts, then still lazy-load the actual script tags so that they only execute when appropriate:
139
+
140
+ ```erb
141
+ <!-- app/views/layouts/application.html.erb -->
142
+ <head>
143
+ <title>My App</title>
144
+
145
+ <%# 1) Set Consent Mode defaults and wire gdpr:accept/decline events %>
146
+ <%= gdpr_consent_mode_defaults(functionality: true, security: true) %>
147
+
148
+ <%# 2) Load GA4 + GTM via consent-gated placeholders %>
149
+ <%= gdpr_lazy_load_tag(:ga4, id: 'G-XXXXXXX') %>
150
+ <%= gdpr_lazy_load_tag(:gtm, id: 'GTM-XXXX') %>
151
+
152
+ <%= csrf_meta_tags %>
153
+ <%= csp_meta_tag %>
154
+ </head>
155
+ ```
156
+
157
+ What this does:
158
+
159
+ - Defines `window.dataLayer` and a `gtag` helper.
160
+ - Sets initial Consent Mode defaults (analytics/marketing denied until consent).
161
+ - Listens to `gdpr:accept` and `gdpr:decline` events and forwards category choices to Google Consent Mode.
162
+ - Uses the same lazy-load placeholders under the hood so GTM/GA only start working when consent is present.
163
+
164
+ ### Step 3: Register custom scripts
165
+
166
+ You can register additional scripts (e.g., Hotjar, reCAPTCHA, custom trackers) in your initializer and then load them with `gdpr_lazy_load_tag`:
167
+
168
+ ```ruby
169
+ # config/initializers/fivo_cookie_consent.rb
170
+ RailsCookiesGdpr.configure do |config|
171
+ # existing options ...
172
+ end
173
+
174
+ # Register custom scripts (using the FivoCookieConsent API)
175
+ RailsCookiesGdpr.register_script(:hotjar, category: :analytics)
176
+ RailsCookiesGdpr.register_script(:recaptcha, category: :functional)
177
+ RailsCookiesGdpr.register_script(
178
+ :adroll,
179
+ category: :marketing,
180
+ src: 'https://example-cdn.com/adroll.js',
181
+ async: true
182
+ )
183
+ ```
184
+
185
+ Then, in your layout or views:
186
+
187
+ ```erb
188
+ <%= gdpr_lazy_load_tag(:hotjar, src: 'https://static.hotjar.com/c/hotjar-XXXX.js') %>
189
+
190
+ <%= gdpr_lazy_load_tag(:recaptcha, src: 'https://www.google.com/recaptcha/api.js', async: true, defer: true) %>
191
+
192
+ <%= gdpr_lazy_load_tag(:adroll) %>
193
+ ```
194
+
195
+ The gem ensures these scripts are only executed once the corresponding category (`analytics`, `functional`, `marketing`, etc.) has been granted.
196
+
197
+ ## Configuration
198
+
199
+ Edit the generated initializer to customize labels and settings:
200
+
201
+ ```ruby
202
+ # config/initializers/fivo_cookie_consent.rb
203
+ RailsCookiesGdpr.configure do |config|
204
+ config.analytics_label = 'Analyse-Cookies'
205
+ config.marketing_label = 'Marketing-Cookies'
206
+ config.functional_label = 'Präferenz-Cookies'
207
+ config.privacy_url = '/datenschutz'
208
+ config.cookie_lifespan = 365 # days
209
+
210
+ # Optional: Customize cookie patterns for auto-detection
211
+ config.cookie_patterns = {
212
+ necessary: [/^_session_/, /^csrf_token/, /^authenticity_token/],
213
+ analytics: [/^_ga/, /^_gid/, /^_gat/, /^_gtag/],
214
+ marketing: [/^_fbp/, /^_fbc/, /^fr$/],
215
+ functional: [/^_locale/, /^language/, /^theme/]
216
+ }
217
+ end
218
+ ```
219
+
220
+ ### Configuration Options
221
+
222
+ All configuration settings are optional and have sensible defaults. The generator creates an initializer with all available options:
223
+
224
+ | Option | Default | Description |
225
+ |--------|---------|-------------|
226
+ | `analytics_label` | `'Analyse-Cookies'` | Display label for analytics cookie category in the banner |
227
+ | `marketing_label` | `'Marketing-Cookies'` | Display label for marketing cookie category in the banner |
228
+ | `functional_label` | `'Präferenz-Cookies'` | Display label for functional cookie category in the banner |
229
+ | `privacy_url` | `'/datenschutz'` | URL path to your privacy policy page (linked from banner) |
230
+ | `cookie_lifespan` | `365` | Number of days until user consent expires and banner reappears |
231
+ | `cookie_patterns` | See below | Hash of regex patterns for automatically categorizing detected cookies |
232
+
233
+ #### Setting Details
234
+
235
+ **Labels (`analytics_label`, `marketing_label`, `functional_label`)**
236
+ - Control the text displayed for each cookie category in the banner
237
+ - Should match your site's language (currently German-focused)
238
+ - Used in both the quick banner and detailed settings modal
239
+
240
+ **Privacy URL (`privacy_url`)**
241
+ - Must be a valid route in your Rails application
242
+ - Banner includes a "Privacy Policy" link pointing to this URL
243
+ - Commonly `/datenschutz`, `/privacy`, or `/impressum`
244
+
245
+ **Cookie Lifespan (`cookie_lifespan`)**
246
+ - Controls when the banner reappears for returning users
247
+ - Stored as `savedAt` timestamp in localStorage
248
+ - Set to `0` to show banner on every visit (not recommended)
249
+
250
+ **Cookie Patterns (`cookie_patterns`)**
251
+ - Hash with keys: `:necessary`, `:analytics`, `:marketing`, `:functional`
252
+ - Each key contains an array of regular expressions
253
+ - Used to automatically categorize detected cookies
254
+ - See "Automatic Cookie Detection" section below for details
255
+
256
+ ## Automatic Cookie Detection
257
+
258
+ The gem automatically detects cookies in your application and categorizes them based on configurable patterns. This feature:
259
+
260
+ - **Scans `document.cookie`** every 2 seconds to detect new cookies
261
+ - **Categorizes cookies** using regular expression patterns
262
+ - **Shows cookie details** in expandable lists with name, duration, and description
263
+ - **Handles empty categories** with appropriate messaging and disabled controls
264
+ - **Development mode** shows an "Uncategorised" section for cookies that don't match any pattern
265
+
266
+ ### Default Cookie Patterns
267
+
268
+ The gem includes sensible defaults for common cookie patterns:
269
+
270
+ ```ruby
271
+ # Necessary cookies (always enabled)
272
+ necessary: [
273
+ /^_session_/, # Rails session cookies
274
+ /^csrf_token/, # CSRF protection
275
+ /^authenticity_token/,
276
+ /^_rails_/, # Rails framework cookies
277
+ /^gdpr_cookie_consent$/ # Our own consent cookie
278
+ ]
279
+
280
+ # Analytics cookies
281
+ analytics: [
282
+ /^_ga/, # Google Analytics
283
+ /^_gid/, /^_gat/, /^_gtag/,
284
+ /^__utm/, # Google Analytics UTM
285
+ /^_dc_gtm_/, # Google Tag Manager
286
+ /^_hj/ # Hotjar
287
+ ]
288
+
289
+ # Marketing cookies
290
+ marketing: [
291
+ /^_fbp/, /^_fbc/, # Facebook Pixel
292
+ /^fr$/, /^tr$/, # Facebook tracking
293
+ /^_pinterest_/, # Pinterest
294
+ /^__Secure-3PAPISID/, # Google Ads
295
+ /^NID$/, /^IDE$/ # Google advertising
296
+ ]
297
+
298
+ # Functional cookies
299
+ functional: [
300
+ /^_locale/, # Language preferences
301
+ /^language/,
302
+ /^theme/, # UI preferences
303
+ /^preferences/,
304
+ /^_user_settings/
305
+ ]
306
+ ```
307
+
308
+ ### Customizing Cookie Patterns
309
+
310
+ You can override or extend the cookie patterns in your initializer:
311
+
312
+ ```ruby
313
+ RailsCookiesGdpr.configure do |config|
314
+ # Add custom patterns
315
+ config.cookie_patterns = {
316
+ necessary: config.default_cookie_patterns[:necessary] + [/^my_app_session/],
317
+ analytics: [/^custom_analytics_/],
318
+ marketing: [/^my_marketing_/],
319
+ functional: [/^user_pref_/]
320
+ }
321
+ end
322
+ ```
323
+
324
+ ## Events
325
+
326
+ The gem dispatches custom events that you can listen to for integrating with analytics:
327
+
328
+ ```javascript
329
+ // Listen for consent acceptance
330
+ document.addEventListener('gdpr:accept', function(event) {
331
+ const { categories } = event.detail;
332
+ // categories = ['analytics', 'marketing', 'functional']
333
+
334
+ if (categories.includes('analytics')) {
335
+ // Initialize Google Analytics, etc.
336
+ gtag('consent', 'update', {
337
+ 'analytics_storage': 'granted'
338
+ });
339
+ }
340
+ });
341
+
342
+ // Listen for consent decline
343
+ document.addEventListener('gdpr:decline', function(event) {
344
+ const { categories } = event.detail;
345
+ // Handle declined categories
346
+ });
347
+
348
+ // Listen for any consent change (recommended for most integrations)
349
+ document.addEventListener('gdpr:change', function(event) {
350
+ const { consent } = event.detail;
351
+ // consent = { necessary: true, analytics: true/false, marketing: true/false, functional: true/false, savedAt: ... }
352
+ });
353
+ ```
354
+
355
+ ### Event Details
356
+
357
+ - **gdpr:accept** - Fired when categories are accepted
358
+ - **gdpr:decline** - Fired when categories are declined
359
+ - **gdpr:change** - Fired whenever consent is updated (accept all, decline all, or custom settings)
360
+ - `event.detail.categories` - Array of affected category names
361
+ - `event.detail.consent` - Full consent object (for `gdpr:change`)
362
+
363
+ ## Cookie Storage
364
+
365
+ User consent is stored in localStorage under the key `gdpr_cookie_consent`:
366
+
367
+ ```json
368
+ {
369
+ "necessary": true,
370
+ "analytics": true,
371
+ "marketing": false,
372
+ "functional": true,
373
+ "savedAt": "2024-01-15T10:30:00.000Z"
374
+ }
375
+ ```
376
+
377
+ ## Accessing Consent Status
378
+
379
+ You can check consent status from JavaScript.
380
+
381
+ ### Recommended: via the `GDPRConsent` facade
382
+
383
+ The gem exposes a small JS helper on `window.GDPRConsent` and as an ES module export:
384
+
385
+ ```javascript
386
+ // Get full consent object
387
+ const consent = GDPRConsent.getConsent();
388
+
389
+ // Check specific category
390
+ if (GDPRConsent.hasConsent('analytics')) {
391
+ // Load analytics scripts
392
+ }
393
+
394
+ // Subscribe to changes (fires on accept/decline/save preferences)
395
+ GDPRConsent.on('gdpr:change', (event) => {
396
+ const { consent } = event.detail;
397
+ console.log('Updated consent:', consent);
398
+ });
399
+
400
+ // Open settings programmatically, e.g. from a footer link
401
+ // GDPRConsent.showModal();
402
+ ```
403
+
404
+ ### Low-level: direct localStorage access
405
+
406
+ If you prefer, you can still read the raw localStorage entry yourself:
407
+
408
+ ```javascript
409
+ // Get full consent object
410
+ const consent = JSON.parse(localStorage.getItem('gdpr_cookie_consent'));
411
+
412
+ // Check specific category
413
+ function hasConsent(category) {
414
+ const consent = JSON.parse(localStorage.getItem('gdpr_cookie_consent') || '{}');
415
+ return consent[category] === true;
416
+ }
417
+
418
+ if (hasConsent('analytics')) {
419
+ // Load analytics scripts
420
+ }
421
+
422
+ // Access detected cookies information
423
+ const detectedCookies = window.RailsCookiesGdpr?.detectCookies?.() || {};
424
+ console.log('Current cookies by category:', detectedCookies);
425
+ ```
426
+
427
+ ## UI Features
428
+
429
+ ### Expandable Category Rows
430
+
431
+ Each cookie category can be expanded to show detailed information about detected cookies:
432
+
433
+ - **Chevron indicators** (▶/▾) show expansion state
434
+ - **Cookie tables** display name, duration, and description
435
+ - **Collapsible interface** keeps the modal clean and focused
436
+ - **No persistence** - expansion state resets on modal close
437
+
438
+ ### Empty State Handling
439
+
440
+ When no cookies are detected in a category:
441
+
442
+ - Shows **"Keine Cookies gesetzt"** message
443
+ - **Disables the toggle** with `aria-disabled="true"`
444
+ - **Automatically enables** when cookies are detected
445
+ - **Maintains accessibility** with proper ARIA attributes
446
+
447
+ ### Development Mode
448
+
449
+ In development environment:
450
+
451
+ - Shows **"Uncategorised"** section for cookies that don't match any pattern
452
+ - Helps identify new cookies that need pattern rules
453
+ - Hidden in production to avoid user confusion
454
+
455
+ ## Styling
456
+
457
+ The gem uses a clean, modern design with the following color scheme:
458
+
459
+ - **Brand Green**: `#1E858B` - Primary buttons and links
460
+ - **Neutral Grey**: `#626465` - Secondary elements and text
461
+
462
+ ### CSS Classes
463
+
464
+ Key CSS classes for customization:
465
+
466
+ - `.gdpr-banner` - Main banner container
467
+ - `.gdpr-modal` - Settings modal
468
+ - `.gdpr-banner__btn--primary` - Accept button style
469
+ - `.gdpr-banner__btn--secondary` - Decline button style
470
+
471
+ ## Browser Support
472
+
473
+ - Chrome 60+
474
+ - Firefox 60+
475
+ - Safari 12+
476
+ - Edge 79+
477
+
478
+ ## Development
479
+
480
+ After checking out the repo, run:
481
+
482
+ ```bash
483
+ bin/setup
484
+ ```
485
+
486
+ To run the test suite:
487
+
488
+ ```bash
489
+ bundle exec rake spec
490
+ ```
491
+
492
+ To run the dummy app for testing:
493
+
494
+ ```bash
495
+ cd spec/dummy
496
+ rails server
497
+ ```
498
+
499
+ ## Contributing
500
+
501
+ 1. Fork the project
502
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
503
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
504
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
505
+ 5. Open a Pull Request
506
+
507
+ ## Release
508
+
509
+ To release a new version:
510
+
511
+ 1. Update the version number in `lib/fivo_cookie_consent/version.rb`
512
+ 2. Update `CHANGELOG.md` with the new changes
513
+ 3. Commit the changes
514
+ 4. Run:
515
+
516
+ ```bash
517
+ bundle exec rake release
518
+ ```
519
+
520
+ This will create a git tag, push commits and tags, and publish the gem to RubyGems.
521
+
522
+ ## License
523
+
524
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ RuboCop::RakeTask.new
10
+
11
+ task default: :spec