Skip to content

Commit c56cfef

Browse files
committed
TrackingConsent: basic version / untested
1 parent 4bc3162 commit c56cfef

File tree

1 file changed

+215
-9
lines changed

1 file changed

+215
-9
lines changed

src/components/TrackingConsent.vue

Lines changed: 215 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,260 @@
33
id="tracking-consent"
44
class="tracking-consent-banner nq-shadow"
55
:class="theme"
6+
v-if="!consentKnown"
67
>
78
{{ text.main }} ❤️
89
<div class="button-group">
10+
<button
11+
class="nq-button-pill light-blue"
12+
@click="allowsConsent"
13+
>{{ text.yes }}</button>
914
<button
1015
class="nq-button-s"
16+
@click="denyConsent"
1117
:class="{ inverse: theme === 'dark' }"
1218
>{{ text.no }}</button>
1319
<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>
1624
</div>
1725
</div>
1826
</template>
1927

2028
<script lang="ts">
2129
import { Component, Prop, Vue } from 'vue-property-decorator';
2230
23-
const STORAGE_KEYS = ['tracking-consent', 'tracking-consensus'];
31+
interface Consents {
32+
allowsBrowserData?: boolean;
33+
allowsUsageData?: boolean;
34+
}
2435
2536
@Component
26-
export default class TrackingConsent extends Vue {
37+
class TrackingConsent extends Vue {
2738
@Prop({
2839
type: Object,
2940
default: () => ({
3041
main: 'Help Nimiq improve by sharing anonymized usage data. Thank you!',
3142
yes: 'Yes',
3243
no: 'No',
44+
browserOnly: 'Browser-info only',
3345
}),
3446
validator: (value) => (
3547
value && typeof value === 'object' &&
36-
value.main && typeof value.main === 'string' && value.main.length &&
48+
value.no && typeof value.no === 'string' && value.no.length &&
3749
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
3952
),
4053
})
41-
private text!: {
54+
private text: {
4255
main: string,
4356
yes: string,
4457
no: string,
58+
browserOnly: string,
4559
};
4660
4761
@Prop({
4862
type: String,
4963
default: 'light',
5064
validator: (theme) => ['dark', 'light'].includes(theme),
5165
})
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+
}
53247
}
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;
54260
</script>
55261

56262
<style scoped>
@@ -84,7 +290,7 @@ export default class TrackingConsent extends Vue {
84290
display: flex;
85291
}
86292
87-
.button-group button:nth-child(2) {
293+
.button-group button:not(:first-child) {
88294
margin-left: 1rem;
89295
}
90296

0 commit comments

Comments
 (0)