|
3 | 3 | id="tracking-consent" |
4 | 4 | class="tracking-consent-banner nq-shadow" |
5 | 5 | :class="theme" |
| 6 | + v-if="!consentKnown" |
6 | 7 | > |
7 | 8 | {{ text.main }} ❤️ |
8 | 9 | <div class="button-group"> |
| 10 | + <button |
| 11 | + class="nq-button-pill light-blue" |
| 12 | + @click="allowsConsent" |
| 13 | + >{{ text.yes }}</button> |
9 | 14 | <button |
10 | 15 | class="nq-button-s" |
| 16 | + @click="denyConsent" |
11 | 17 | :class="{ inverse: theme === 'dark' }" |
12 | 18 | >{{ text.no }}</button> |
13 | 19 | <button |
14 | | - class="nq-button-pill light-blue" |
15 | | - >{{ text.yes }}</button> |
| 20 | + class="nq-button-s" |
| 21 | + @click="allowsBrowserData" |
| 22 | + :class="{ inverse: theme === 'dark' }" |
| 23 | + >{{ text.browserOnly }}</button> |
16 | 24 | </div> |
17 | 25 | </div> |
18 | 26 | </template> |
19 | 27 |
|
20 | 28 | <script lang="ts"> |
21 | 29 | import { Component, Prop, Vue } from 'vue-property-decorator'; |
22 | 30 |
|
23 | | -const STORAGE_KEYS = ['tracking-consent', 'tracking-consensus']; |
| 31 | +interface Consents { |
| 32 | + allowsBrowserData?: boolean; |
| 33 | + allowsUsageData?: boolean; |
| 34 | +} |
24 | 35 |
|
25 | 36 | @Component |
26 | | -export default class TrackingConsent extends Vue { |
| 37 | +class TrackingConsent extends Vue { |
27 | 38 | @Prop({ |
28 | 39 | type: Object, |
29 | 40 | default: () => ({ |
30 | 41 | main: 'Help Nimiq improve by sharing anonymized usage data. Thank you!', |
31 | 42 | yes: 'Yes', |
32 | 43 | no: 'No', |
| 44 | + browserOnly: 'Browser-info only', |
33 | 45 | }), |
34 | 46 | validator: (value) => ( |
35 | 47 | value && typeof value === 'object' && |
36 | | - value.main && typeof value.main === 'string' && value.main.length && |
| 48 | + value.no && typeof value.no === 'string' && value.no.length && |
37 | 49 | value.yes && typeof value.yes === 'string' && value.yes.length && |
38 | | - value.no && typeof value.no === 'string' && value.no.length |
| 50 | + value.main && typeof value.main === 'string' && value.main.length && |
| 51 | + value.browserOnly && typeof value.browserOnly === 'string' && value.browserOnly.length |
39 | 52 | ), |
40 | 53 | }) |
41 | | - private text!: { |
| 54 | + private text: { |
42 | 55 | main: string, |
43 | 56 | yes: string, |
44 | 57 | no: string, |
| 58 | + browserOnly: string, |
45 | 59 | }; |
46 | 60 |
|
47 | 61 | @Prop({ |
48 | 62 | type: String, |
49 | 63 | default: 'light', |
50 | 64 | validator: (theme) => ['dark', 'light'].includes(theme), |
51 | 65 | }) |
52 | | - public theme!: string; |
| 66 | + private theme: string; |
| 67 | +
|
| 68 | + @Prop({ |
| 69 | + type: String, |
| 70 | + default: 'nimiq.com', |
| 71 | + }) |
| 72 | + private domain: string; |
| 73 | +
|
| 74 | + @Prop({ |
| 75 | + type: Object, |
| 76 | + default: () => ({ |
| 77 | + setTrackerUrl: TrackingConsent.MATOMO_URL + 'nimiq.php', |
| 78 | + }), |
| 79 | + }) |
| 80 | + private options: { |
| 81 | + setSiteId: string, // 3 for safe.nimiq.com ? |
| 82 | + setTrackerUrl: string |
| 83 | + addDownloadExtensions?: string, |
| 84 | + trackPageView?: boolean, |
| 85 | + enableLinkTracking?: boolean, |
| 86 | + [k: string]: string | boolean, |
| 87 | + }; |
| 88 | +
|
| 89 | + @Prop({ |
| 90 | + type: String, |
| 91 | + default: 'nimiq.js', |
| 92 | + }) |
| 93 | + private tagManagerScript: string; |
| 94 | +
|
| 95 | + private consentKnown: boolean = false; |
| 96 | + private _storage: Consents; |
| 97 | +
|
| 98 | + private async mounted() { |
| 99 | + if (!window.startTime) { |
| 100 | + window.startTime = (new Date().getTime()); |
| 101 | + } |
| 102 | +
|
| 103 | + if (this.consents.allowsBrowserData || this.consents.allowsUsageData) { |
| 104 | + this._initMatomo(); |
| 105 | + } |
| 106 | +
|
| 107 | + const geoIpResponse = await fetch(TrackingConsent.GEOIP_SERVER); |
| 108 | + if (geoIpResponse.status !== 200) { |
| 109 | + throw new Error('Failed to contact geoip server'); |
| 110 | + } |
| 111 | + const geoIpInfo = await geoIpResponse.json(); |
| 112 | + if (geoIpInfo.continent !== 'EU') { |
| 113 | + this.allowsConsent(); |
| 114 | + } |
| 115 | + } |
| 116 | +
|
| 117 | + public get consents(): Consents { |
| 118 | + if (this._storage) { |
| 119 | + return this._storage; |
| 120 | + } |
| 121 | +
|
| 122 | + const cookie = this._getCookie(TrackingConsent.STORAGE_KEYS.MAIN); |
| 123 | + if (cookie) { |
| 124 | + this._storage = JSON.parse(cookie); |
| 125 | + return this._storage; |
| 126 | + } |
| 127 | +
|
| 128 | + const localStoredConsent = |
| 129 | + localStorage.getItem(TrackingConsent.STORAGE_KEYS.MAIN) || |
| 130 | + localStorage.getItem(TrackingConsent.STORAGE_KEYS.SECOND); |
| 131 | +
|
| 132 | + if (localStoredConsent) { |
| 133 | + this._storage = JSON.parse(localStoredConsent); |
| 134 | + this._setCookie(TrackingConsent.STORAGE_KEYS.MAIN, localStoredConsent); |
| 135 | + localStorage.removeItem(TrackingConsent.STORAGE_KEYS.MAIN); |
| 136 | + localStorage.removeItem(TrackingConsent.STORAGE_KEYS.SECOND); |
| 137 | + return this._storage; |
| 138 | + } |
| 139 | +
|
| 140 | + return {}; |
| 141 | + } |
| 142 | +
|
| 143 | + public trackEvent( |
| 144 | + category: string, |
| 145 | + action: string, |
| 146 | + name?: string, |
| 147 | + value?: string | number, |
| 148 | + ): void { |
| 149 | + const _paq = window.paq || []; |
| 150 | + const obj: Array<string | number> = [category, action]; |
| 151 | +
|
| 152 | + if (name) { |
| 153 | + obj.push(name); |
| 154 | + } |
| 155 | + if (value) { |
| 156 | + obj.push(value); |
| 157 | + } |
| 158 | +
|
| 159 | + _paq.push(obj); |
| 160 | + } |
| 161 | +
|
| 162 | + private denyConsent(): void { |
| 163 | + this._setConsent({ allowsUsageData: false, allowsBrowserData: false }); |
| 164 | + } |
| 165 | +
|
| 166 | + private allowsConsent(): void { |
| 167 | + this._setConsent({ allowsUsageData: true, allowsBrowserData: true }); |
| 168 | + this._initMatomo(); |
| 169 | + } |
| 170 | +
|
| 171 | + private allowsBrowserData(): void { |
| 172 | + this._setConsent({ allowsBrowserData: true }); |
| 173 | + this._initMatomo(); |
| 174 | + } |
| 175 | +
|
| 176 | + private _setConsent(consent: Consents): void { |
| 177 | + this._setCookie(TrackingConsent.STORAGE_KEYS.MAIN, JSON.stringify(consent)); |
| 178 | + this.consentKnown = true; |
| 179 | + } |
| 180 | +
|
| 181 | + private _initMatomo() { |
| 182 | + // initialize matomo |
| 183 | + const _paq = window._paq || []; |
| 184 | + const _mtm = window._mtm || []; |
| 185 | +
|
| 186 | + // set mtm start |
| 187 | + _mtm.push({ |
| 188 | + 'mtm.startTime': window.startTime, |
| 189 | + 'event': 'mtm.Start', |
| 190 | + }); |
| 191 | +
|
| 192 | + // Get referrer from localstorage |
| 193 | + const referrer = localStorage.getItem('referrer'); |
| 194 | + if (referrer) { |
| 195 | + _paq.push(['setReferrerUrl', decodeURIComponent(referrer)]); |
| 196 | + localStorage.removeItem('referrer'); |
| 197 | + } |
| 198 | +
|
| 199 | + // Cycle through options and set them |
| 200 | + Object.keys(this.options).forEach((k) => { |
| 201 | + const option = this.options[k]; |
| 202 | +
|
| 203 | + if (option) { |
| 204 | + _paq.push(typeof option === 'boolean' ? [k] : [k, option]); |
| 205 | + } |
| 206 | + }); |
| 207 | +
|
| 208 | + // append script |
| 209 | + (function() { |
| 210 | + const g = document.createElement('script'); |
| 211 | + const s = document.getElementsByTagName('script')[0]; |
| 212 | + g.type = 'text/javascript'; g.async = true; g.defer = true; |
| 213 | + g.src = TrackingConsent.MATOMO_URL + this.tagManagerScript; |
| 214 | + s.parentNode.insertBefore(g, s); |
| 215 | + })(); |
| 216 | + } |
| 217 | +
|
| 218 | + private _setCookie( |
| 219 | + cookieName: string, |
| 220 | + cookieValue: string, |
| 221 | + expirationDays?: number, |
| 222 | + ): void { |
| 223 | + const cookie = [cookieName + '=' + cookieValue]; |
| 224 | +
|
| 225 | + if (expirationDays) { |
| 226 | + const date = new Date(); |
| 227 | + date.setTime(date.getTime() + (expirationDays * 24 * 60 * 60 * 1000)); |
| 228 | +
|
| 229 | + cookie.push(';expires=' + date.toUTCString()); |
| 230 | + } |
| 231 | +
|
| 232 | + cookie.push('path=/'); |
| 233 | + cookie.push('domain=' + this.domain); |
| 234 | +
|
| 235 | + document.cookie = cookie.join(';'); |
| 236 | + } |
| 237 | +
|
| 238 | + private _getCookie(cookieName: string): string { |
| 239 | + return document.cookie.split('; ').map((c) => { |
| 240 | + const i = c.indexOf('='); |
| 241 | + const key = c.substring(0, i); |
| 242 | + const value = c.substring(i); |
| 243 | +
|
| 244 | + return [key, value]; |
| 245 | + }).reduce((acc, cur) => (acc[cur[0]] = cur[1], acc), {})[cookieName] || ''; |
| 246 | + } |
53 | 247 | } |
| 248 | +
|
| 249 | +namespace TrackingConsent { // tslint:disable-line:no-namespace |
| 250 | + export enum STORAGE_KEYS { |
| 251 | + MAIN = 'tracking-consent', |
| 252 | + SECOND = 'tracking-consensus', |
| 253 | + } |
| 254 | +
|
| 255 | + export const GEOIP_SERVER = 'https://geoip.nimiq-network.com:8443/v1/locate'; |
| 256 | + export const MATOMO_URL = '//stats.nimiq-network.com/'; |
| 257 | +} |
| 258 | +
|
| 259 | +export default TrackingConsent; |
54 | 260 | </script> |
55 | 261 |
|
56 | 262 | <style scoped> |
@@ -84,7 +290,7 @@ export default class TrackingConsent extends Vue { |
84 | 290 | display: flex; |
85 | 291 | } |
86 | 292 |
|
87 | | - .button-group button:nth-child(2) { |
| 293 | + .button-group button:not(:first-child) { |
88 | 294 | margin-left: 1rem; |
89 | 295 | } |
90 | 296 |
|
|
0 commit comments