consently 0.1.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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +279 -0
  5. data/Rakefile +6 -0
  6. data/app/assets/stylesheets/consently.css +252 -0
  7. data/app/controllers/consently/application_controller.rb +4 -0
  8. data/app/controllers/consently/consents_controller.rb +44 -0
  9. data/app/helpers/consently/application_helper.rb +4 -0
  10. data/app/helpers/consently/tags_helper.rb +176 -0
  11. data/app/javascript/consently/banner_controller.js +190 -0
  12. data/app/models/consently/application_record.rb +5 -0
  13. data/app/models/consently/consent_record.rb +18 -0
  14. data/app/views/consently/_banner.html.erb +93 -0
  15. data/app/views/consently/_policy.html.erb +85 -0
  16. data/config/importmap.rb +3 -0
  17. data/config/locales/cs.yml +33 -0
  18. data/config/locales/de.yml +33 -0
  19. data/config/locales/en.yml +33 -0
  20. data/config/locales/es.yml +33 -0
  21. data/config/locales/fr.yml +33 -0
  22. data/config/locales/hu.yml +33 -0
  23. data/config/locales/it.yml +33 -0
  24. data/config/locales/nl.yml +33 -0
  25. data/config/locales/pl.yml +33 -0
  26. data/config/locales/sk.yml +33 -0
  27. data/config/routes.rb +5 -0
  28. data/lib/consently/configuration.rb +150 -0
  29. data/lib/consently/consent.rb +62 -0
  30. data/lib/consently/cookie.rb +12 -0
  31. data/lib/consently/engine.rb +33 -0
  32. data/lib/consently/providers/base.rb +95 -0
  33. data/lib/consently/providers/clarity.rb +34 -0
  34. data/lib/consently/providers/custom.rb +34 -0
  35. data/lib/consently/providers/google_ads.rb +32 -0
  36. data/lib/consently/providers/google_analytics.rb +45 -0
  37. data/lib/consently/providers/google_tag_manager.rb +34 -0
  38. data/lib/consently/providers/hotjar.rb +39 -0
  39. data/lib/consently/providers/meta_pixel.rb +37 -0
  40. data/lib/consently/providers/plausible.rb +30 -0
  41. data/lib/consently/providers.rb +9 -0
  42. data/lib/consently/script.rb +17 -0
  43. data/lib/consently/version.rb +3 -0
  44. data/lib/consently.rb +74 -0
  45. data/lib/generators/consently/consent_log/consent_log_generator.rb +28 -0
  46. data/lib/generators/consently/consent_log/templates/create_consently_consent_records.rb +19 -0
  47. data/lib/generators/consently/install/install_generator.rb +44 -0
  48. data/lib/generators/consently/install/templates/consently.rb +63 -0
  49. data/lib/generators/consently/views/views_generator.rb +15 -0
  50. data/lib/tasks/consently_tasks.rake +4 -0
  51. metadata +121 -0
@@ -0,0 +1,176 @@
1
+ module Consently
2
+ # The three helpers a host application calls: the tags in <head>, the
3
+ # noscript fallbacks right after <body>, and the banner anywhere on the page.
4
+ module TagsHelper
5
+ # Every tag configured for this request. Tags whose category the visitor
6
+ # has not agreed to are rendered inert (type="text/plain") and the banner
7
+ # turns them into real scripts the moment consent is given - so a visitor
8
+ # who accepts does not have to reload to be counted.
9
+ def consently_tags
10
+ return "".html_safe unless consently_enabled?
11
+
12
+ parts = []
13
+ parts << consently_stylesheet_tag if Consently.config.stylesheet
14
+ parts << consently_consent_mode_tag if Consently.config.google_consent_mode
15
+ Consently.tags_for(request).each do |provider|
16
+ granted = consently_consent.granted?(provider.category)
17
+ provider.scripts.each { |script| parts << consently_script_tag(script, provider, granted) }
18
+ end
19
+
20
+ safe_join(parts, "\n")
21
+ end
22
+
23
+ # Goes directly after <body> - Google Tag Manager and Meta both still ship
24
+ # a noscript fallback. Only rendered for categories already granted: there
25
+ # is no way to hold an iframe back and release it later.
26
+ def consently_noscript_tags
27
+ return "".html_safe unless consently_enabled?
28
+
29
+ fallbacks = Consently.tags_for(request).filter_map do |provider|
30
+ next unless consently_consent.granted?(provider.category)
31
+
32
+ provider.noscript&.html_safe
33
+ end
34
+ return "".html_safe if fallbacks.empty?
35
+
36
+ content_tag(:noscript, safe_join(fallbacks, "\n"))
37
+ end
38
+
39
+ # The banner, the preferences panel, and the JavaScript that releases the
40
+ # blocked tags. Render it once per page, ideally at the end of the body.
41
+ #
42
+ # The container stays in the DOM after a choice is made so that
43
+ # `consently_preferences_link` has something to reopen.
44
+ def consently_banner(policy_url: nil)
45
+ return "".html_safe unless consently_enabled? && Consently.consent_required?(request)
46
+
47
+ render "consently/banner",
48
+ consent: consently_consent,
49
+ policy_url: policy_url || consently_policy_url,
50
+ categories: Consently.config.optional_categories
51
+ end
52
+
53
+ # A "Cookie settings" link for the footer. Reopens the panel.
54
+ #
55
+ # Marked rather than wired: the link lives outside the banner element, so
56
+ # a data-action on it would never bind. The controller watches the whole
57
+ # document for a click on anything carrying this attribute, which also
58
+ # means your own markup can reopen the panel just by wearing it.
59
+ def consently_preferences_link(name = nil, **options, &block)
60
+ name ||= t("consently.preferences_link")
61
+ options[:data] = { consently_open: true }.merge(options[:data] || {})
62
+
63
+ link_to(name, "#consently", options, &block)
64
+ end
65
+
66
+ def consently_consent
67
+ @consently_consent ||= if Consently.consent_required?(request)
68
+ Consent.from_cookie(cookies[Consently.config.cookie_name], version: Consently.config.consent_version)
69
+ else
70
+ # Nobody to ask, so nothing is held back.
71
+ Consent.new(categories: Consently.config.categories, version: Consently.config.consent_version)
72
+ end
73
+ end
74
+
75
+ # Push an event onto the dataLayer from a view, respecting consent: with
76
+ # no analytics consent the event is simply not emitted.
77
+ #
78
+ # <%= consently_data_layer_push("purchase", value: 120, currency: "EUR") %>
79
+ def consently_data_layer_push(event, category: :analytics, **payload)
80
+ return "".html_safe unless consently_enabled? && consently_consent.granted?(category)
81
+
82
+ payload = payload.merge(event: event)
83
+ consently_inline_script "window.dataLayer = window.dataLayer || []; window.dataLayer.push(#{payload.to_json});"
84
+ end
85
+
86
+ # A complete cookie policy for the tags this request would load: every
87
+ # category, every vendor, every cookie it sets and for how long, plus
88
+ # whether the visitor has agreed to it right now.
89
+ #
90
+ # Drop it into your own policy page under your own heading and legal text.
91
+ def consently_policy
92
+ render "consently/policy", tags: Consently.tags_for(request), consent: consently_consent
93
+ end
94
+
95
+ # The banner brings its own plain CSS - no framework, no build step. The
96
+ # look is driven by custom properties, so overriding a few variables is
97
+ # usually enough; `rails g consently:views` is there for the rest.
98
+ def consently_stylesheet_tag
99
+ stylesheet_link_tag "consently", media: "all"
100
+ end
101
+
102
+ # Where the banner POSTs the decision, when consent logging is on and the
103
+ # engine is mounted. Nil otherwise, and the banner skips the request.
104
+ def consently_log_url
105
+ return nil unless Consently.config.log_consents
106
+
107
+ consently.consents_path
108
+ rescue NoMethodError, NameError
109
+ nil
110
+ end
111
+
112
+ private
113
+
114
+ def consently_enabled?
115
+ Consently.enabled?(request)
116
+ end
117
+
118
+ # Google's consent mode v2 defaults. This one is never blocked: its whole
119
+ # job is to tell Google's tags that they may not store anything yet, and
120
+ # it has to be on the page before them.
121
+ def consently_consent_mode_tag
122
+ analytics = consently_consent.granted?(:analytics) ? "granted" : "denied"
123
+ marketing = consently_consent.granted?(:marketing) ? "granted" : "denied"
124
+
125
+ consently_inline_script <<~JS
126
+ window.dataLayer = window.dataLayer || [];
127
+ function gtag(){dataLayer.push(arguments);}
128
+ gtag('consent', 'default', {
129
+ 'ad_storage': 'denied',
130
+ 'ad_user_data': 'denied',
131
+ 'ad_personalization': 'denied',
132
+ 'analytics_storage': 'denied',
133
+ 'functionality_storage': 'granted',
134
+ 'security_storage': 'granted',
135
+ 'wait_for_update': 500
136
+ });
137
+ #{"gtag('consent', 'update', { 'analytics_storage': '#{analytics}', 'ad_storage': '#{marketing}', 'ad_user_data': '#{marketing}', 'ad_personalization': '#{marketing}' });" if consently_consent.given?}
138
+ JS
139
+ end
140
+
141
+ # A plain <script> with the JS as written. javascript_tag would wrap it in
142
+ # a CDATA comment nobody has needed since XHTML.
143
+ def consently_inline_script(javascript)
144
+ attributes = {}
145
+ attributes[:nonce] = content_security_policy_nonce if content_security_policy_nonce.present?
146
+
147
+ content_tag(:script, javascript.html_safe, attributes)
148
+ end
149
+
150
+ def consently_policy_url
151
+ url = Consently.config.policy_url
152
+ url.respond_to?(:call) ? url.call(self) : url
153
+ end
154
+
155
+ def consently_script_tag(script, provider, granted)
156
+ attributes = { data: { "consently-category" => provider.category }.merge(script.data_attributes) }
157
+ attributes[:nonce] = content_security_policy_nonce if content_security_policy_nonce.present?
158
+
159
+ if granted
160
+ attributes[:src] = script.src if script.external?
161
+ attributes[:async] = true if script.async
162
+ attributes[:defer] = true if script.defer
163
+ content_tag(:script, script.inline&.html_safe, attributes)
164
+ else
165
+ # Inert until the visitor agrees: browsers neither execute nor fetch a
166
+ # script of an unknown type, and the src lives in a data attribute so
167
+ # it is not even requested.
168
+ attributes[:type] = "text/plain"
169
+ attributes[:data]["consently-src"] = script.src if script.external?
170
+ attributes[:data]["consently-async"] = "true" if script.async
171
+ attributes[:data]["consently-defer"] = "true" if script.defer
172
+ content_tag(:script, script.inline&.html_safe, attributes)
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,190 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // The banner, the preferences panel, and the part that actually matters:
4
+ // turning the blocked <script type="text/plain"> tags into live ones the
5
+ // moment the visitor agrees, so nobody has to reload to be counted.
6
+ export default class extends Controller {
7
+ static targets = ["card", "preferences", "category", "settingsButton", "saveButton", "cancelButton"]
8
+
9
+ // The panel slides open on a grid row, and things disappear by class rather
10
+ // than by inline style. Both class names come from the markup, so restyling
11
+ // the banner never means touching this file.
12
+ static classes = ["open", "hidden"]
13
+
14
+ static values = {
15
+ cookie: { type: String, default: "consently" },
16
+ version: { type: String, default: "1" },
17
+ maxAge: { type: Number, default: 60 * 60 * 24 * 180 },
18
+ path: { type: String, default: "/" },
19
+ categories: Array,
20
+ googleConsentMode: { type: Boolean, default: true },
21
+ respectDoNotTrack: { type: Boolean, default: false },
22
+ respectGpc: { type: Boolean, default: true },
23
+ reload: { type: Boolean, default: false },
24
+ // Whether the visitor had already chosen when the page was rendered, so
25
+ // closing the panel knows whether to leave the banner behind or not.
26
+ decided: { type: Boolean, default: false },
27
+ logUrl: String,
28
+ nonce: String
29
+ }
30
+
31
+ connect() {
32
+ // An opt-out signal is an answer, so the banner never appears for it.
33
+ if (this.#optedOut()) this.rejectAll()
34
+ }
35
+
36
+ acceptAll() {
37
+ this.#store(this.categoriesValue)
38
+ }
39
+
40
+ rejectAll() {
41
+ this.#store([])
42
+ }
43
+
44
+ // Anything anywhere on the page carrying data-consently-open reopens the
45
+ // panel - a footer link is outside this controller's element, so it cannot
46
+ // carry an action of its own.
47
+ openFromLink(event) {
48
+ if (!event.target.closest("[data-consently-open]")) return
49
+
50
+ this.openPreferences(event)
51
+ }
52
+
53
+ openPreferences(event) {
54
+ if (event) event.preventDefault()
55
+
56
+ this.cardTarget.classList.remove(...this.hiddenClasses)
57
+ this.preferencesTarget.classList.add(...this.openClasses)
58
+ this.settingsButtonTarget.classList.add(...this.hiddenClasses)
59
+ this.settingsButtonTarget.setAttribute("aria-expanded", "true")
60
+ this.saveButtonTarget.classList.remove(...this.hiddenClasses)
61
+ this.cancelButtonTarget.classList.remove(...this.hiddenClasses)
62
+
63
+ // Someone who reopened this from a link in the footer is now looking at a
64
+ // panel their screen reader has not been told about.
65
+ this.cardTarget.focus({ preventScroll: true })
66
+ }
67
+
68
+ // Out of the panel without deciding anything: back to the short banner, or
69
+ // out of the way entirely if the visitor had already chosen before.
70
+ closePreferences() {
71
+ this.#collapse()
72
+ if (this.decidedValue) this.cardTarget.classList.add(...this.hiddenClasses)
73
+ }
74
+
75
+ savePreferences() {
76
+ this.#store(this.categoryTargets.filter((box) => box.checked).map((box) => box.value))
77
+ }
78
+
79
+ // Consent is written, the page catches up, and the choice is logged - in
80
+ // that order, so a failing log request cannot cost the visitor their click.
81
+ #store(categories) {
82
+ this.#writeCookie(categories)
83
+ this.#activateScripts(categories)
84
+ this.#updateGoogleConsent(categories)
85
+ this.#logConsent(categories)
86
+
87
+ this.#collapse()
88
+ this.cardTarget.classList.add(...this.hiddenClasses)
89
+ this.decidedValue = true
90
+
91
+ this.dispatch("change", { detail: { categories }, target: document, prefix: "consently" })
92
+
93
+ // Last, so anything listening for the event has already run.
94
+ if (this.reloadValue) window.location.reload()
95
+ }
96
+
97
+ #collapse() {
98
+ this.preferencesTarget.classList.remove(...this.openClasses)
99
+ this.settingsButtonTarget.classList.remove(...this.hiddenClasses)
100
+ this.settingsButtonTarget.setAttribute("aria-expanded", "false")
101
+ this.saveButtonTarget.classList.add(...this.hiddenClasses)
102
+ this.cancelButtonTarget.classList.add(...this.hiddenClasses)
103
+ }
104
+
105
+ #writeCookie(categories) {
106
+ const value = JSON.stringify({ v: this.versionValue, c: categories, t: new Date().toISOString() })
107
+ const secure = window.location.protocol === "https:" ? "; Secure" : ""
108
+
109
+ document.cookie = `${this.cookieValue}=${encodeURIComponent(value)}; path=${this.pathValue}; max-age=${this.maxAgeValue}; SameSite=Lax${secure}`
110
+ }
111
+
112
+ #activateScripts(categories) {
113
+ const granted = new Set([...categories, "necessary"])
114
+
115
+ document.querySelectorAll('script[type="text/plain"][data-consently-category]').forEach((blocked) => {
116
+ if (!granted.has(blocked.dataset.consentlyCategory)) return
117
+
118
+ const script = document.createElement("script")
119
+ // Copy everything the server put on the tag except our own bookkeeping,
120
+ // so vendor attributes like data-domain survive.
121
+ for (const { name, value } of blocked.attributes) {
122
+ if (name === "type" || name.startsWith("data-consently-")) continue
123
+ script.setAttribute(name, value)
124
+ }
125
+ script.dataset.consentlyCategory = blocked.dataset.consentlyCategory
126
+
127
+ if (this.nonceValue) script.setAttribute("nonce", this.nonceValue)
128
+ if (blocked.dataset.consentlySrc) {
129
+ script.src = blocked.dataset.consentlySrc
130
+ } else {
131
+ script.textContent = blocked.textContent
132
+ }
133
+ if (blocked.dataset.consentlyAsync) script.async = true
134
+ if (blocked.dataset.consentlyDefer) script.defer = true
135
+
136
+ blocked.replaceWith(script)
137
+ })
138
+ }
139
+
140
+ // Google's consent mode v2: the page loaded with everything denied, this
141
+ // lifts exactly what was granted.
142
+ #updateGoogleConsent(categories) {
143
+ if (!this.googleConsentModeValue) return
144
+
145
+ const analytics = categories.includes("analytics") ? "granted" : "denied"
146
+ const marketing = categories.includes("marketing") ? "granted" : "denied"
147
+
148
+ window.dataLayer = window.dataLayer || []
149
+ // gtag pushes its `arguments` object, not an array - Google's tags read it
150
+ // by position and an array is not the same thing to them.
151
+ const gtag = function () { window.dataLayer.push(arguments) }
152
+ gtag("consent", "update", {
153
+ analytics_storage: analytics,
154
+ ad_storage: marketing,
155
+ ad_user_data: marketing,
156
+ ad_personalization: marketing
157
+ })
158
+ window.dataLayer.push({ event: "consently_consent_update", consently_categories: categories.join(",") })
159
+ }
160
+
161
+ async #logConsent(categories) {
162
+ if (!this.logUrlValue) return
163
+
164
+ try {
165
+ await fetch(this.logUrlValue, {
166
+ method: "POST",
167
+ // The page may be reloading a moment later; keepalive means the
168
+ // request still reaches us instead of being cancelled.
169
+ keepalive: true,
170
+ headers: {
171
+ "Content-Type": "application/json",
172
+ "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')?.content ?? ""
173
+ },
174
+ body: JSON.stringify({ categories, version: this.versionValue })
175
+ })
176
+ } catch (error) {
177
+ // A lost log entry is not worth breaking the page over.
178
+ console.warn("[consently] could not record consent", error)
179
+ }
180
+ }
181
+
182
+ // Global Privacy Control is a binding opt-out signal in California and
183
+ // Colorado; Do Not Track is advisory, which is why that one is opt-in.
184
+ #optedOut() {
185
+ if (this.respectGpcValue && navigator.globalPrivacyControl === true) return true
186
+
187
+ return this.respectDoNotTrackValue &&
188
+ [navigator.doNotTrack, window.doNotTrack, navigator.msDoNotTrack].includes("1")
189
+ }
190
+ }
@@ -0,0 +1,5 @@
1
+ module Consently
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ module Consently
2
+ # Proof that a visitor agreed to something, and to what. Optional: create it
3
+ # with `rails g consently:consent_log` and switch config.log_consents on.
4
+ class ConsentRecord < ApplicationRecord
5
+ self.table_name = "consently_consent_records"
6
+
7
+ scope :for_version, ->(version) { where(consent_version: version.to_s) }
8
+ scope :in_scope, ->(scope) { where(scope: scope.to_s) }
9
+
10
+ def categories
11
+ self[:categories].to_s.split(",").map(&:to_sym)
12
+ end
13
+
14
+ def granted?(category)
15
+ category.to_sym == Consent::NECESSARY || categories.include?(category.to_sym)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,93 @@
1
+ <%# Plain CSS classes, no framework: see app/assets/stylesheets/consently.css.
2
+ The container stays in the DOM after a choice is made so that
3
+ `consently_preferences_link` has something to reopen. Run
4
+ `rails g consently:views` to take this file over into your own app. %>
5
+ <div id="consently"
6
+ class="consently"
7
+ data-turbo-permanent
8
+ data-controller="consently-banner"
9
+ data-action="click@document->consently-banner#openFromLink"
10
+ data-consently-banner-open-class="consently__panel--open"
11
+ data-consently-banner-hidden-class="consently-hidden"
12
+ data-consently-banner-cookie-value="<%= Consently.config.cookie_name %>"
13
+ data-consently-banner-version-value="<%= Consently.config.consent_version %>"
14
+ data-consently-banner-max-age-value="<%= Consently.config.cookie_max_age %>"
15
+ data-consently-banner-path-value="<%= Consently.config.cookie_path %>"
16
+ data-consently-banner-categories-value="<%= categories.map(&:to_s).to_json %>"
17
+ data-consently-banner-google-consent-mode-value="<%= Consently.config.google_consent_mode %>"
18
+ data-consently-banner-respect-do-not-track-value="<%= Consently.config.respect_do_not_track %>"
19
+ data-consently-banner-respect-gpc-value="<%= Consently.config.respect_global_privacy_control %>"
20
+ data-consently-banner-reload-value="<%= Consently.config.reload_after_choice %>"
21
+ data-consently-banner-decided-value="<%= consent.given? %>"
22
+ data-consently-banner-log-url-value="<%= consently_log_url %>"
23
+ data-consently-banner-nonce-value="<%= content_security_policy_nonce %>">
24
+ <%# A non-modal dialog: it must be announced and reachable, but it must not
25
+ trap anyone who came to read the page. tabindex lets the reopen link put
26
+ focus here without adding the card to the tab order. %>
27
+ <div class="consently__card <%= "consently-hidden" if consent.given? %>"
28
+ data-consently-banner-target="card"
29
+ role="dialog"
30
+ aria-modal="false"
31
+ aria-labelledby="consently-message"
32
+ tabindex="-1">
33
+ <p class="consently__text" id="consently-message">
34
+ <%= t("consently.banner.message") %>
35
+ <% if policy_url.present? %>
36
+ <%= link_to t("consently.banner.policy_link"), policy_url, class: "consently__link" %>
37
+ <% end %>
38
+ </p>
39
+
40
+ <div class="consently__panel" id="consently-preferences" data-consently-banner-target="preferences">
41
+ <div class="consently__panel-inner">
42
+ <div class="consently__options">
43
+ <label class="consently__option">
44
+ <%# Always on, and shown that way rather than hidden: people look for it. %>
45
+ <%= tag.input type: "checkbox", checked: true, disabled: true %>
46
+ <span>
47
+ <span class="consently__option-name"><%= t("consently.categories.necessary.name") %></span>
48
+ <span class="consently__option-description"><%= t("consently.categories.necessary.description") %></span>
49
+ </span>
50
+ </label>
51
+
52
+ <% categories.each do |category| %>
53
+ <label class="consently__option">
54
+ <%= tag.input type: "checkbox", value: category, checked: consent.granted?(category),
55
+ id: "consently-#{category}", data: { consently_banner_target: "category" } %>
56
+ <span>
57
+ <span class="consently__option-name"><%= t("consently.categories.#{category}.name", default: category.to_s.humanize) %></span>
58
+ <span class="consently__option-description"><%= t("consently.categories.#{category}.description", default: "") %></span>
59
+ </span>
60
+ </label>
61
+ <% end %>
62
+ </div>
63
+ </div>
64
+ </div>
65
+
66
+ <div class="consently__actions">
67
+ <button type="button" data-action="consently-banner#acceptAll" class="consently__button consently__button--primary">
68
+ <%= t("consently.banner.accept_all") %>
69
+ </button>
70
+ <button type="button" data-action="consently-banner#rejectAll" class="consently__button">
71
+ <%= t("consently.banner.reject_all") %>
72
+ </button>
73
+ <button type="button" data-action="consently-banner#openPreferences"
74
+ data-consently-banner-target="settingsButton"
75
+ aria-expanded="false"
76
+ aria-controls="consently-preferences"
77
+ class="consently__button consently__button--quiet">
78
+ <%= t("consently.banner.preferences") %>
79
+ </button>
80
+ <button type="button" data-action="consently-banner#savePreferences"
81
+ data-consently-banner-target="saveButton"
82
+ class="consently__button consently-hidden">
83
+ <%= t("consently.banner.save") %>
84
+ </button>
85
+ <%# Only offered while the panel is open - a way back out of it. %>
86
+ <button type="button" data-action="consently-banner#closePreferences"
87
+ data-consently-banner-target="cancelButton"
88
+ class="consently__button consently__button--quiet consently-hidden">
89
+ <%= t("consently.banner.cancel") %>
90
+ </button>
91
+ </div>
92
+ </div>
93
+ </div>
@@ -0,0 +1,85 @@
1
+ <%# A cookie policy that cannot go stale: it is rendered from the same
2
+ configuration the tags are, so a tag added to the initializer shows up
3
+ here, in the right category, with the cookies it sets.
4
+
5
+ Plain CSS, scoped under .consently-policy - see the gem's stylesheet.
6
+ `rails g consently:views` to take it over. %>
7
+ <div class="consently-policy">
8
+ <% Consently.config.categories.each do |category| %>
9
+ <% providers = tags.select { |provider| provider.category == category } %>
10
+
11
+ <section class="consently-policy__category">
12
+ <div class="consently-policy__heading">
13
+ <h2 class="consently-policy__title"><%= t("consently.categories.#{category}.name", default: category.to_s.humanize) %></h2>
14
+ <% if consent.granted?(category) %>
15
+ <span class="consently-policy__status consently-policy__status--granted"><%= t("consently.policy.granted") %></span>
16
+ <% else %>
17
+ <span class="consently-policy__status"><%= t("consently.policy.denied") %></span>
18
+ <% end %>
19
+ </div>
20
+ <p class="consently-policy__description"><%= t("consently.categories.#{category}.description", default: "") %></p>
21
+
22
+ <% if providers.empty? %>
23
+ <p class="consently-policy__empty"><%= t("consently.policy.no_tags") %></p>
24
+ <% else %>
25
+ <% providers.each do |provider| %>
26
+ <div class="consently-policy__vendor">
27
+ <p class="consently-policy__vendor-name">
28
+ <%= provider.key.to_s.titleize %>
29
+ <span class="consently-policy__vendor-id"><%= provider.options[:id] || provider.options[:domain] %></span>
30
+ </p>
31
+
32
+ <% if provider.cookies.empty? %>
33
+ <p class="consently-policy__empty"><%= t("consently.policy.no_cookies") %></p>
34
+ <% else %>
35
+ <table class="consently-policy__table">
36
+ <thead>
37
+ <tr>
38
+ <th><%= t("consently.policy.cookie_name") %></th>
39
+ <th><%= t("consently.policy.cookie_duration") %></th>
40
+ </tr>
41
+ </thead>
42
+ <tbody>
43
+ <% provider.cookies.each do |cookie| %>
44
+ <tr>
45
+ <td><code><%= cookie.name %></code></td>
46
+ <td>
47
+ <%= cookie.session? ? t("consently.policy.duration_session") : t("consently.policy.duration_days", count: cookie.days) %>
48
+ </td>
49
+ </tr>
50
+ <% end %>
51
+ </tbody>
52
+ </table>
53
+ <% end %>
54
+ </div>
55
+ <% end %>
56
+ <% end %>
57
+ </section>
58
+ <% end %>
59
+
60
+ <section class="consently-policy__category">
61
+ <h2 class="consently-policy__title"><%= t("consently.policy.own_heading") %></h2>
62
+ <p class="consently-policy__description"><%= t("consently.policy.own_body") %></p>
63
+
64
+ <table class="consently-policy__table">
65
+ <thead>
66
+ <tr>
67
+ <th><%= t("consently.policy.cookie_name") %></th>
68
+ <th><%= t("consently.policy.cookie_duration") %></th>
69
+ <th><%= t("consently.policy.own_version") %></th>
70
+ </tr>
71
+ </thead>
72
+ <tbody>
73
+ <tr>
74
+ <td><code><%= Consently.config.cookie_name %></code></td>
75
+ <td><%= t("consently.policy.duration_days", count: Consently.config.cookie_max_age / 86_400) %></td>
76
+ <td><%= Consently.config.consent_version %></td>
77
+ </tr>
78
+ </tbody>
79
+ </table>
80
+ </section>
81
+
82
+ <p class="consently-policy__footer">
83
+ <%= consently_preferences_link %>
84
+ </p>
85
+ </div>
@@ -0,0 +1,3 @@
1
+ # Pinned for the host application, so `import "consently/banner_controller"`
2
+ # resolves without anyone editing their own importmap.
3
+ pin "consently/banner_controller", to: "consently/banner_controller.js", preload: true
@@ -0,0 +1,33 @@
1
+ cs:
2
+ consently:
3
+ banner:
4
+ message: "Používáme soubory cookie, aby tento web fungoval, a s vaším souhlasem také k tomu, abychom rozuměli, jak jej používáte, a mohli vám ukazovat relevantní nabídky."
5
+ policy_link: "Přečtěte si naše zásady cookies."
6
+ accept_all: "Přijmout vše"
7
+ reject_all: "Odmítnout vše"
8
+ preferences: "Nastavení"
9
+ save: "Uložit volbu"
10
+ cancel: "Zrušit"
11
+ preferences_link: "Nastavení cookies"
12
+ policy:
13
+ granted: "souhlas udělen"
14
+ denied: "bez souhlasu"
15
+ no_tags: "V této kategorii nic není."
16
+ no_cookies: "Nenastavuje žádné cookies."
17
+ cookie_name: "Cookie"
18
+ cookie_duration: "Doba uložení"
19
+ duration_days: "%{count} dní"
20
+ duration_session: "do zavření prohlížeče"
21
+ own_heading: "Samotná cookie se souhlasem"
22
+ own_body: "Vaše volba je uložená v jedné cookie ve vašem prohlížeči: kategorie, se kterými jste souhlasili, verze těchto zásad a čas. Nic o vás."
23
+ own_version: "Verze zásad"
24
+ categories:
25
+ necessary:
26
+ name: "Nezbytné"
27
+ description: "Nutné pro fungování webu. Nelze je vypnout."
28
+ analytics:
29
+ name: "Analytické"
30
+ description: "Ukazují nám, jak web používáte, abychom jej mohli zlepšovat."
31
+ marketing:
32
+ name: "Marketingové"
33
+ description: "Umožňují nám ukazovat vám nabídky, které odpovídají vašim zájmům."
@@ -0,0 +1,33 @@
1
+ de:
2
+ consently:
3
+ banner:
4
+ message: "Wir verwenden Cookies, damit diese Website funktioniert - und mit Ihrer Einwilligung, um zu verstehen, wie sie genutzt wird, und Ihnen passende Angebote zu zeigen."
5
+ policy_link: "Zur Cookie-Richtlinie."
6
+ accept_all: "Alle akzeptieren"
7
+ reject_all: "Alle ablehnen"
8
+ preferences: "Einstellungen"
9
+ save: "Auswahl speichern"
10
+ cancel: "Abbrechen"
11
+ preferences_link: "Cookie-Einstellungen"
12
+ policy:
13
+ granted: "eingewilligt"
14
+ denied: "nicht eingewilligt"
15
+ no_tags: "Nichts in dieser Kategorie."
16
+ no_cookies: "Setzt keine Cookies."
17
+ cookie_name: "Cookie"
18
+ cookie_duration: "Speicherdauer"
19
+ duration_days: "%{count} Tage"
20
+ duration_session: "bis zum Schließen des Browsers"
21
+ own_heading: "Das Einwilligungs-Cookie selbst"
22
+ own_body: "Ihre Wahl liegt in einem Cookie in Ihrem Browser: die Kategorien, denen Sie zugestimmt haben, die Version dieser Richtlinie und die Uhrzeit. Nichts über Sie."
23
+ own_version: "Version der Richtlinie"
24
+ categories:
25
+ necessary:
26
+ name: "Notwendig"
27
+ description: "Für den Betrieb der Website erforderlich. Sie lassen sich nicht abschalten."
28
+ analytics:
29
+ name: "Statistik"
30
+ description: "Zeigen uns, wie die Website genutzt wird, damit wir sie verbessern können."
31
+ marketing:
32
+ name: "Marketing"
33
+ description: "Erlauben uns, Ihnen Angebote zu zeigen, die zu Ihren Interessen passen."