diff --git a/.env b/.env
index 39fb11d32..adf125a51 100644
--- a/.env
+++ b/.env
@@ -1,6 +1,9 @@
-API_URL=""
+API_URL="http://localhost:8000"
USE_AXE=""
SENTRY_DSN=""
SENTRY_ENV=""
-REACT_APP_VERSION=""
FEEDBACK_EMAILS=""
+MATOMO_SRC_URL=""
+MATOMO_URL_BASE=""
+MATOMO_SITE_ID=""
+MATOMO_ENABLED=false
diff --git a/cypress/index.d.ts b/cypress/index.d.ts
index 10e6d884f..4ed1c8eb6 100644
--- a/cypress/index.d.ts
+++ b/cypress/index.d.ts
@@ -41,5 +41,6 @@ declare namespace Cypress {
hours: string;
minutes: string;
}) => Chainable;
+ acceptAllCookies(): void;
}
}
diff --git a/cypress/integration/App.spec.ts b/cypress/integration/App.spec.ts
index 21ca8e85b..dc9eb6ab8 100644
--- a/cypress/integration/App.spec.ts
+++ b/cypress/integration/App.spec.ts
@@ -5,6 +5,7 @@
describe('Open aukiolot app', () => {
beforeEach(() => {
cy.visitResourcePageAsAuthenticatedUser(Cypress.env('resourceId'));
+ cy.acceptAllCookies();
});
it('Contains correct page title', () => {
diff --git a/cypress/integration/CopyResourcePeriods.spec.ts b/cypress/integration/CopyResourcePeriods.spec.ts
index 18b5980d3..fcc3b96aa 100644
--- a/cypress/integration/CopyResourcePeriods.spec.ts
+++ b/cypress/integration/CopyResourcePeriods.spec.ts
@@ -8,6 +8,8 @@ describe('User visits resource page with target resource', () => {
Cypress.env('resourceId'),
Cypress.env('targetResourceId')
);
+
+ cy.acceptAllCookies();
});
it('Contains copy section title and copy button', () => {
diff --git a/cypress/integration/ExceptionOpeningHours.ts b/cypress/integration/ExceptionOpeningHours.ts
index 045e04044..1e8659a5c 100644
--- a/cypress/integration/ExceptionOpeningHours.ts
+++ b/cypress/integration/ExceptionOpeningHours.ts
@@ -3,6 +3,7 @@
describe('User adds a new exception date period', () => {
beforeEach(() => {
cy.visitResourcePageAsAuthenticatedUser(Cypress.env('resourceId'));
+ cy.acceptAllCookies();
});
it('Users successfully adds a new exception opening hours', () => {
diff --git a/cypress/integration/Holidays.ts b/cypress/integration/Holidays.ts
index 7067b0da2..d3bd079f5 100644
--- a/cypress/integration/Holidays.ts
+++ b/cypress/integration/Holidays.ts
@@ -4,6 +4,7 @@ describe('User adds a new holiday opening period', () => {
beforeEach(() => {
cy.visitResourcePageAsAuthenticatedUser(Cypress.env('resourceId'));
cy.intercept('POST', '/v1/date_period/').as('saveHoliday');
+ cy.acceptAllCookies();
});
it('Users successfully adds a new holiday opening hours', () => {
diff --git a/cypress/integration/NormalOpeningHours.ts b/cypress/integration/NormalOpeningHours.ts
index 33d9a8780..86a8667f2 100644
--- a/cypress/integration/NormalOpeningHours.ts
+++ b/cypress/integration/NormalOpeningHours.ts
@@ -3,6 +3,7 @@
describe('User adds a new opening period', () => {
beforeEach(() => {
cy.visitResourcePageAsAuthenticatedUser(Cypress.env('resourceId'));
+ cy.acceptAllCookies();
});
it('Users successfully adds a new opening hours', () => {
diff --git a/cypress/integration/User.spec.ts b/cypress/integration/User.spec.ts
index 6eb51bf1d..e9fc2967d 100644
--- a/cypress/integration/User.spec.ts
+++ b/cypress/integration/User.spec.ts
@@ -3,6 +3,7 @@
describe('Unauthenticated user', () => {
beforeEach(() => {
cy.visitResourcePageAsUnauthenticatedUser(Cypress.env('resourceId'));
+ cy.acceptAllCookies();
});
it('Is redirected to unauthenticated page', () => {
@@ -13,6 +14,7 @@ describe('Unauthenticated user', () => {
describe('Authenticated user', () => {
beforeEach(() => {
cy.visitResourcePageAsAuthenticatedUser(Cypress.env('resourceId'));
+ cy.acceptAllCookies();
});
it('should logout permanently on application close', () => {
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index 0f44e7a3e..3ac9eb188 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -222,3 +222,12 @@ Cypress.Commands.add(
cy.get(`#${id}-minutes`).type(minutes);
}
);
+
+Cypress.Commands.add('acceptAllCookies', () => {
+ const cookieConsentModal = cy.get('#cookie-consent-content');
+ const acceptAllButton = cookieConsentModal.get(
+ 'button[data-testid="cookie-consent-approve-button"]'
+ );
+
+ acceptAllButton.click();
+});
diff --git a/docker-compose.yml b/docker-compose.yml
index eb221bf83..eab4b8d27 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,8 +1,12 @@
-version: "3.7"
+version: '3.7'
services:
app:
+ platform: linux/amd64
container_name: hauki-admin-ui
build:
context: .
+ env_file:
+ - .env
+ - .env.local
ports:
- '3000:8000'
diff --git a/package.json b/package.json
index 8e183714d..a069b5f16 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"cypress-axe": "^0.12.1",
"date-fns": "2.29.3",
"date-holidays": "^3.16.1",
+ "dotenv-cli": "^7.3.0",
"enzyme": "^3.11.0",
"eslint-config-airbnb-typescript-prettier": "^5.0.0",
"hds-core": "^3.5.0",
@@ -47,10 +48,10 @@
"typescript": "4.4.3"
},
"scripts": {
- "start": "REACT_APP_VERSION=$npm_package_version SENTRY_ENV=${SENTRY_ENV:=local} API_URL=${API_URL:=http://localhost:8000} USE_AXE=${USE_AXE:=true} ./scripts/env.sh && cp env-config.js ./public/ && BROWSER=none react-scripts start",
- "start-with-test-api": "REACT_APP_VERSION=$npm_package_version SENTRY_ENV=local API_URL=https://hauki-api.test.hel.ninja USE_AXE=${USE_AXE:=true} ./scripts/env.sh && cp env-config.js ./public/ && react-scripts start",
- "start-with-new-test-api": "REACT_APP_VERSION=$npm_package_version SENTRY_ENV=local API_URL=https://hauki-api.test.hel.ninja USE_AXE=${USE_AXE:=true} ./scripts/env.sh && cp env-config.js ./public/ && react-scripts start",
- "build": "REACT_APP_VERSION=$npm_package_version react-scripts build",
+ "start": "dotenv -c -v REACT_APP_VERSION=$npm_package_version -- ./scripts/env.sh && cp env-config.js ./public/ && BROWSER=none react-scripts start",
+ "start-with-test-api": "dotenv -c -v REACT_APP_VERSION=$npm_package_version SENTRY_ENV=local API_URL=https://hauki-api.test.hel.ninja USE_AXE=${USE_AXE:=true} -- ./scripts/env.sh && cp env-config.js ./public/ && react-scripts start",
+ "start-with-new-test-api": "dotenv -c -v REACT_APP_VERSION=$npm_package_version SENTRY_ENV=local API_URL=https://hauki-api.test.hel.ninja USE_AXE=${USE_AXE:=true} -- ./scripts/env.sh && cp env-config.js ./public/ && react-scripts start",
+ "build": "dotenv -c -v REACT_APP_VERSION=$npm_package_version -- react-scripts build",
"test": "react-scripts test --env=jest-environment-jsdom-sixteen",
"test:cov": "react-scripts test --env=jest-environment-jsdom-sixteen --coverage",
"test-cypress": "start-server-and-test start http://localhost:3000 cypress-run-chrome",
diff --git a/src/App.tsx b/src/App.tsx
index f8e3e5aa3..9da0d6f34 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -30,6 +30,10 @@ import EditHolidaysPage from './pages/EditHolidaysPage';
import AddExceptionOpeningHoursPage from './pages/AddExceptionOpeningHoursPage';
import EditExceptionOpeningHoursPage from './pages/EditExceptionOpeningHoursPage';
import { SelectedDatePeriodsProvider } from './common/selectedDatePeriodsContext/SelectedDatePeriodsContext';
+import MatomoTracker from './components/matomo/MatomoTracker';
+import MatomoContext from './components/matomo/matomo-context';
+import CookieConsent from './components/cookie-consent/CookieConsent';
+import CookieManagement from './pages/CookieManagement';
type OptionalAuthTokens = AuthTokens | undefined;
@@ -94,240 +98,265 @@ const App = (): JSX.Element => {
[authTokens, clearAuth]
);
+ const matomoTracker = useMemo(
+ () =>
+ new MatomoTracker({
+ urlBase: window?.ENV?.MATOMO_URL_BASE,
+ siteId: window?.ENV?.MATOMO_SITE_ID,
+ srcUrl: window?.ENV?.MATOMO_SRC_URL,
+ enabled: window?.ENV?.MATOMO_ENABLED,
+ configurations: {
+ setDoNotTrack: true,
+ },
+ }),
+ []
+ );
+
return (
-
-
-
-
-
-
- Etusivu
-
-
-
-
-
-
- Kohdetta ei löydy
-
- Kohdetta ei löytynyt. Yritä myöhemmin uudestaan. Ongelman
- toistuessa ota yhteys sivuston ylläpitoon. Teidät on
- automaattisesti kirjattu ulos.
-
-
-
-
-
-
-
- Puutteelliset tunnukset
-
-
-
-
-
-
- Puuttuvat tunnukset
-
-
-
- ) => {
- const { id, childId } = match.params;
+
+
- return (
- id && (
-
-
-
-
-
-
-
- )
- );
- }}
- />
- ) => {
- const { id } = match.params;
+
+
+
+
+
+
+ Etusivu
+
+
+
+
+
+
+ Kohdetta ei löydy
+
+ Kohdetta ei löytynyt. Yritä myöhemmin uudestaan.
+ Ongelman toistuessa ota yhteys sivuston ylläpitoon.
+ Teidät on automaattisesti kirjattu ulos.
+
+
+
+
+
+
+
+ Puutteelliset tunnukset
+
+
+
+
+
+
+ Puuttuvat tunnukset
+
+
+
+
+
+
+
+
+
+
+ ) => {
+ const { id, childId } = match.params;
- return (
- id && (
-
-
-
-
-
-
-
- )
- );
- }}
- />
- ) => {
- const { id } = match.params;
+ return (
+ id && (
+
+
+
+
+
+
+
+ )
+ );
+ }}
+ />
+ ) => {
+ const { id } = match.params;
- return (
- id && (
- <>
-
-
-
-
- >
- )
- );
- }}
- />
- ) => {
- const { id, datePeriodId } = match.params;
+ return (
+ id && (
+
+
+
+
+
+
+
+ )
+ );
+ }}
+ />
+ ) => {
+ const { id } = match.params;
- return (
- id &&
- datePeriodId && (
- <>
-
-
-
-
- >
- )
- );
- }}
- />
- ) => {
- const { id } = match.params;
+ return (
+ id && (
+ <>
+
+
+
+
+ >
+ )
+ );
+ }}
+ />
+ ) => {
+ const { id, datePeriodId } = match.params;
- return (
- id && (
- <>
-
-
-
-
- >
- )
- );
- }}
- />
- ) => {
- const { id } = match.params;
+ return (
+ id &&
+ datePeriodId && (
+ <>
+
+
+
+
+ >
+ )
+ );
+ }}
+ />
+ ) => {
+ const { id } = match.params;
+
+ return (
+ id && (
+ <>
+
+
+
+
+ >
+ )
+ );
+ }}
+ />
+ ) => {
+ const { id } = match.params;
- return (
- id && (
- <>
-
-
-
-
- >
- )
- );
- }}
- />
- ) => {
- const { id, datePeriodId } = match.params;
+ return (
+ id && (
+ <>
+
+
+
+
+ >
+ )
+ );
+ }}
+ />
+ ) => {
+ const { id, datePeriodId } = match.params;
- return (
- id &&
- datePeriodId && (
- <>
-
-
-
-
- >
- )
- );
- }}
- />
-
-
-
+ return (
+ id &&
+ datePeriodId && (
+ <>
+
+
+
+
+ >
+ )
+ );
+ }}
+ />
+
+
+
+
);
diff --git a/src/components/cookie-consent/CookieConsent.test.tsx b/src/components/cookie-consent/CookieConsent.test.tsx
new file mode 100644
index 000000000..ce27693f7
--- /dev/null
+++ b/src/components/cookie-consent/CookieConsent.test.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { AppContext } from '../../App-context';
+import CookieConsent from './CookieConsent';
+import { Language } from '../../common/lib/types';
+import MatomoContext from '../matomo/matomo-context';
+import MatomoTracker from '../matomo/MatomoTracker';
+
+describe('CookieConsent', () => {
+ it('should render cookie modal', async () => {
+ global.ResizeObserver = jest.fn().mockImplementation(() => ({
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ disconnect: jest.fn(),
+ }));
+
+ const setLanguageMock = jest.fn();
+ const mockMatomoTracker = new MatomoTracker({
+ urlBase: 'https://www.test.fi/',
+ siteId: 'test123',
+ srcUrl: 'test.js',
+ enabled: true,
+ });
+
+ render(
+
+
+
+
+
+ );
+
+ const buttons = await screen.findAllByRole('button');
+ const changeLanguageButton = buttons[0];
+
+ userEvent.click(changeLanguageButton);
+
+ const enOption = await screen.findByRole('link', { name: 'English (EN)' });
+
+ userEvent.click(enOption);
+
+ expect(setLanguageMock).toHaveBeenCalled();
+ });
+});
diff --git a/src/components/cookie-consent/CookieConsent.tsx b/src/components/cookie-consent/CookieConsent.tsx
new file mode 100644
index 000000000..d038c387c
--- /dev/null
+++ b/src/components/cookie-consent/CookieConsent.tsx
@@ -0,0 +1,17 @@
+/* eslint-disable no-underscore-dangle */
+import { CookieModal } from 'hds-react';
+import React, { FC } from 'react';
+import { useAppContext } from '../../App-context';
+import useCookieConsent from './hooks/useCookieConsent';
+
+const CookieConsent: FC = () => {
+ const { language, setLanguage } = useAppContext();
+ const { config } = useCookieConsent({
+ language,
+ setLanguage,
+ });
+
+ return ;
+};
+
+export default CookieConsent;
diff --git a/src/components/cookie-consent/hooks/useCookieConsent.ts b/src/components/cookie-consent/hooks/useCookieConsent.ts
new file mode 100644
index 000000000..9b2cc1280
--- /dev/null
+++ b/src/components/cookie-consent/hooks/useCookieConsent.ts
@@ -0,0 +1,79 @@
+/* eslint-disable no-underscore-dangle */
+import { useEffect, useState } from 'react';
+import { ContentSource } from 'hds-react';
+import { Language } from '../../../common/lib/types';
+
+type Props = {
+ language: Language | undefined;
+ setLanguage: ((language: Language) => void) | undefined;
+ isModal?: boolean;
+};
+
+const useCookieConsent = ({
+ language,
+ setLanguage,
+ isModal = true,
+}: Props): { config: ContentSource } => {
+ const [currentLanguage, setCurrentLanguage] = useState(
+ language ?? Language.FI
+ );
+
+ useEffect(() => {
+ if (language) {
+ const newLanguage =
+ Language[language.toUpperCase() as keyof typeof Language];
+
+ if (newLanguage !== currentLanguage) {
+ setCurrentLanguage(newLanguage);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [language]);
+
+ const onLanguageChange = (lang: string) => {
+ const newLanguage = Language[lang.toUpperCase() as keyof typeof Language];
+
+ setCurrentLanguage(newLanguage);
+
+ if (setLanguage) {
+ setLanguage(newLanguage);
+ }
+ };
+
+ const config: ContentSource = {
+ siteName: 'Aukiolot',
+ currentLanguage,
+ optionalCookies: {
+ groups: [
+ {
+ commonGroup: 'statistics',
+ cookies: [{ commonCookie: 'matomo' }],
+ },
+ ],
+ },
+ language: { onLanguageChange },
+ onAllConsentsGiven: (consents) => {
+ if (consents.matomo) {
+ // start tracking
+ window._paq.push(['setConsentGiven']);
+ window._paq.push(['setCookieConsentGiven']);
+ }
+ },
+ onConsentsParsed: (consents) => {
+ /* istanbul ignore next */
+ if (consents.matomo === undefined) {
+ // tell matomo to wait for consent:
+ window._paq.push(['requireConsent']);
+ window._paq.push(['requireCookieConsent']);
+ } else if (consents.matomo === false) {
+ // tell matomo to forget conset
+ window._paq.push(['forgetConsentGiven']);
+ }
+ },
+ focusTargetSelector: isModal ? `#main` : undefined,
+ };
+
+ return { config };
+};
+
+export default useCookieConsent;
diff --git a/src/components/footer/HaukiFooter.tsx b/src/components/footer/HaukiFooter.tsx
index b4ece7c92..937315233 100644
--- a/src/components/footer/HaukiFooter.tsx
+++ b/src/components/footer/HaukiFooter.tsx
@@ -38,6 +38,7 @@ const HaukiFooter = (): JSX.Element => {
target="_blank"
label={t('Footer.ContentLicenseLink')}
/>
+
>
diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx
index 4f1552b4c..12499faa6 100644
--- a/src/components/main/Main.tsx
+++ b/src/components/main/Main.tsx
@@ -1,5 +1,8 @@
-import React, { ReactNode } from 'react';
+import React, { ReactNode, useEffect } from 'react';
import './Main.scss';
+import { useCookies } from 'hds-react';
+import { useLocation } from 'react-router-dom';
+import useMatomo from '../matomo/hooks/useMatomo';
type MainProps = {
id: string;
@@ -10,10 +13,25 @@ export function MainContainer({ children }: Partial): JSX.Element {
return {children}
;
}
-const Main = ({ id, children }: MainProps): JSX.Element => (
-
- {children}
-
-);
+const Main = ({ id, children }: MainProps): JSX.Element => {
+ const location = useLocation();
+ const { getAllConsents } = useCookies();
+ const { trackPageView } = useMatomo();
+
+ useEffect(() => {
+ if (getAllConsents().matomo) {
+ trackPageView({
+ href: window.location.href,
+ });
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [getAllConsents, location.pathname, location.search]);
+
+ return (
+
+ {children}
+
+ );
+};
export default Main;
diff --git a/src/components/matomo/MatomoTracker.test.ts b/src/components/matomo/MatomoTracker.test.ts
new file mode 100644
index 000000000..abba3ec7d
--- /dev/null
+++ b/src/components/matomo/MatomoTracker.test.ts
@@ -0,0 +1,45 @@
+/* eslint-disable no-underscore-dangle */
+import MatomoTracker, { MatomoTrackerOptions } from './MatomoTracker';
+
+describe('MatomoTracker', () => {
+ it('should initialise window._paq', () => {
+ window._paq = [];
+
+ const intance = new MatomoTracker({
+ urlBase: 'https://www.test.fi/',
+ siteId: 'test123',
+ srcUrl: 'test.js',
+ enabled: true,
+ configurations: {
+ foo: 'bar',
+ testArray: ['testArrayItem1', 'testArrayItem2'],
+ testNoValue: undefined,
+ },
+ });
+
+ expect(intance).toBeTruthy();
+ expect(window._paq).toEqual([
+ ['setTrackerUrl', 'https://www.test.fi/matomo.php'],
+ ['setSiteId', 'test123'],
+ ['foo', 'bar'],
+ ['testArray', 'testArrayItem1', 'testArrayItem2'],
+ ['testNoValue'],
+ ['enableLinkTracking', true],
+ ]);
+ });
+
+ it('should throw error if urlBase missing', () => {
+ expect(
+ () => new MatomoTracker({ siteId: 'test123' } as MatomoTrackerOptions)
+ ).toThrowError();
+ });
+
+ it('should throw error if siteId missing', () => {
+ expect(
+ () =>
+ new MatomoTracker({
+ urlBase: 'https://www.test.fi',
+ } as MatomoTrackerOptions)
+ ).toThrowError();
+ });
+});
diff --git a/src/components/matomo/MatomoTracker.ts b/src/components/matomo/MatomoTracker.ts
new file mode 100644
index 000000000..0d00e10da
--- /dev/null
+++ b/src/components/matomo/MatomoTracker.ts
@@ -0,0 +1,128 @@
+/* eslint-disable no-underscore-dangle */
+import { TRACK_TYPES } from './constants';
+
+export type MatomoTrackerOptions = {
+ urlBase: string;
+ siteId: string;
+ srcUrl: string;
+ trackerUrl?: string;
+ enabled: boolean;
+ linkTracking?: boolean;
+ configurations?: {
+ [key: string]: string | string[] | boolean | undefined;
+ };
+};
+
+export type CustomDimension = {
+ id: number;
+ value: string;
+};
+
+export type TrackPageViewParams = {
+ documentTitle?: string;
+ href?: string | Location;
+ customDimensions?: boolean | CustomDimension[];
+};
+
+export type TrackParams = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ data: any[];
+} & TrackPageViewParams;
+
+class MatomoTracker {
+ constructor(userOptions: MatomoTrackerOptions) {
+ if (!userOptions.urlBase) {
+ throw new Error('Matomo urlBase is required');
+ }
+
+ if (!userOptions.siteId) {
+ throw new Error('Matomo siteId is required.');
+ }
+
+ this.initialize(userOptions);
+ }
+
+ private initialize({
+ urlBase,
+ siteId,
+ srcUrl,
+ trackerUrl = 'matomo.php',
+ enabled = true,
+ linkTracking = true,
+ configurations = {},
+ }: MatomoTrackerOptions) {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ window._paq = window._paq || [];
+
+ if (window._paq.length !== 0) {
+ return;
+ }
+
+ if (!enabled) {
+ return;
+ }
+
+ this.pushInstruction('setTrackerUrl', `${urlBase}${trackerUrl}`);
+ this.pushInstruction('setSiteId', siteId);
+
+ Object.entries(configurations).forEach(([name, instructions]) => {
+ if (instructions instanceof Array) {
+ this.pushInstruction(name, ...instructions);
+ } else if (instructions === undefined) {
+ this.pushInstruction(name);
+ } else {
+ this.pushInstruction(name, instructions);
+ }
+ });
+
+ this.enableLinkTracking(linkTracking);
+
+ const doc = document;
+ const scriptElement = doc.createElement('script');
+ const scripts = doc.getElementsByTagName('script')[0];
+
+ scriptElement.type = 'text/javascript';
+ scriptElement.async = true;
+ scriptElement.defer = true;
+ scriptElement.src = `${urlBase}${srcUrl}`;
+
+ if (scripts?.parentNode) {
+ scripts?.parentNode.insertBefore(scriptElement, scripts);
+ }
+ }
+
+ enableLinkTracking(active: boolean): void {
+ this.pushInstruction('enableLinkTracking', active);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ pushInstruction(name: string, ...args: any[]): this {
+ if (typeof window !== 'undefined') {
+ window._paq.push([name, ...args]);
+ }
+
+ return this;
+ }
+
+ trackPageView(params?: TrackPageViewParams): void {
+ this.track({ data: [TRACK_TYPES.TRACK_VIEW], ...params });
+ }
+
+ track({
+ data = [],
+ documentTitle = document.title,
+ href,
+ }: TrackParams): void {
+ if (data.length) {
+ this.pushInstruction('setCustomUrl', href ?? window.location.href);
+ this.pushInstruction('setDocumentTitle', documentTitle);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ this.pushInstruction(...(data as [string, ...any[]]));
+ }
+ }
+}
+
+export default MatomoTracker;
diff --git a/src/components/matomo/constants.ts b/src/components/matomo/constants.ts
new file mode 100644
index 000000000..a75c675de
--- /dev/null
+++ b/src/components/matomo/constants.ts
@@ -0,0 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export, @typescript-eslint/naming-convention
+export const TRACK_TYPES = {
+ TRACK_VIEW: 'trackPageView',
+};
diff --git a/src/components/matomo/hooks/useMatomo.test.tsx b/src/components/matomo/hooks/useMatomo.test.tsx
new file mode 100644
index 000000000..3377c6ad0
--- /dev/null
+++ b/src/components/matomo/hooks/useMatomo.test.tsx
@@ -0,0 +1,52 @@
+import React, { useEffect } from 'react';
+import { render } from '@testing-library/react';
+import * as MatomoTracker from '../MatomoTracker';
+import { MatomoProvider } from '../matomo-context';
+import useMatomo from './useMatomo';
+
+describe('useMatomo', () => {
+ const MockedComponent = () => {
+ const { trackPageView } = useMatomo();
+
+ useEffect(() => {
+ trackPageView({ href: 'https://www.hel.fi' });
+ }, [trackPageView]);
+
+ return MockedComponent
;
+ };
+
+ it('should trackPageView', () => {
+ const trackPageViewMock = jest.fn();
+
+ jest.spyOn(MatomoTracker, 'default').mockImplementation(
+ () =>
+ ({
+ trackPageView: trackPageViewMock,
+ } as unknown as MatomoTracker.default)
+ );
+
+ // eslint-disable-next-line new-cap
+ const instance = new MatomoTracker.default({
+ urlBase: 'https://www.hel.fi',
+ siteId: 'test123',
+ srcUrl: 'test.js',
+ enabled: true,
+ });
+
+ const MockProvider = () => {
+ return (
+
+
+
+ );
+ };
+
+ expect(MatomoTracker.default).toHaveBeenCalled();
+
+ render();
+
+ expect(trackPageViewMock).toHaveBeenCalledWith({
+ href: 'https://www.hel.fi',
+ });
+ });
+});
diff --git a/src/components/matomo/hooks/useMatomo.ts b/src/components/matomo/hooks/useMatomo.ts
new file mode 100644
index 000000000..7b9272e8d
--- /dev/null
+++ b/src/components/matomo/hooks/useMatomo.ts
@@ -0,0 +1,17 @@
+import { useCallback, useContext } from 'react';
+import MatomoContext from '../matomo-context';
+import { TrackPageViewParams } from '../MatomoTracker';
+import { MatomoTrackerInstance } from '../types';
+
+function useMatomo(): MatomoTrackerInstance {
+ const instance = useContext(MatomoContext);
+
+ const trackPageView = useCallback(
+ (params?: TrackPageViewParams) => instance?.trackPageView(params),
+ [instance]
+ );
+
+ return { trackPageView };
+}
+
+export default useMatomo;
diff --git a/src/components/matomo/matomo-context.tsx b/src/components/matomo/matomo-context.tsx
new file mode 100644
index 000000000..7292ca385
--- /dev/null
+++ b/src/components/matomo/matomo-context.tsx
@@ -0,0 +1,20 @@
+import React, { createContext } from 'react';
+import { MatomoTrackerInstance } from './types';
+
+export type MatomoProviderProps = {
+ children?: React.ReactNode;
+ value: MatomoTrackerInstance;
+};
+
+const MatomoContext = createContext(null);
+
+export const MatomoProvider: React.FC = ({
+ children,
+ value,
+}) => {
+ const Context = MatomoContext;
+
+ return {children};
+};
+
+export default MatomoContext;
diff --git a/src/components/matomo/types.ts b/src/components/matomo/types.ts
new file mode 100644
index 000000000..9306f1755
--- /dev/null
+++ b/src/components/matomo/types.ts
@@ -0,0 +1,5 @@
+import MatomoTracker from './MatomoTracker';
+
+export type MatomoTrackerInstance = {
+ trackPageView: MatomoTracker['trackPageView'];
+};
diff --git a/src/globals.d.ts b/src/globals.d.ts
index e449ce1c7..2d3d94834 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -6,5 +6,11 @@ interface Window {
SENTRY_ENV: string;
REACT_APP_VERSION: string;
FEEDBACK_EMAILS: string;
+ MATOMO_SRC_URL: string;
+ MATOMO_URL_BASE: string;
+ MATOMO_SITE_ID: string;
+ MATOMO_ENABLED: boolean;
};
+ // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any
+ _paq: [string, ...any[]][];
}
diff --git a/src/language/en.json b/src/language/en.json
index cd4b7dcb6..e7db72e6d 100644
--- a/src/language/en.json
+++ b/src/language/en.json
@@ -111,6 +111,7 @@
"BackToTopLabel": "Back to top",
"AccessibilityStatementLink": "Accessibility statement",
"AccessibilityStatementLinkUrl": "https://kaupunkialustana.hel.fi/aukiolosovellus/accessibility-statement",
- "ContentLicenseLink": "Content license CC BY 4.0"
+ "ContentLicenseLink": "Content license CC BY 4.0",
+ "CookiesLink": "Cookie settings"
}
-}
\ No newline at end of file
+}
diff --git a/src/language/fi.json b/src/language/fi.json
index 418140de9..e07a119ec 100644
--- a/src/language/fi.json
+++ b/src/language/fi.json
@@ -112,6 +112,7 @@
"BackToTopLabel": "Takaisin ylös",
"AccessibilityStatementLink": "Saavutettavuusseloste",
"AccessibilityStatementLinkUrl": "https://kaupunkialustana.hel.fi/aukiolosovellus/saavutettavuusseloste",
- "ContentLicenseLink": "Sisältölisenssi CC BY 4.0"
+ "ContentLicenseLink": "Sisältölisenssi CC BY 4.0",
+ "CookiesLink": "Evästeasetukset"
}
-}
\ No newline at end of file
+}
diff --git a/src/language/sv.json b/src/language/sv.json
index d4fed596f..f1dcebcd3 100644
--- a/src/language/sv.json
+++ b/src/language/sv.json
@@ -112,6 +112,7 @@
"BackToTopLabel": "Tillbaka till toppen",
"AccessibilityStatementLink": "Tillgänglighetsutlåtande",
"AccessibilityStatementLinkUrl": "https://kaupunkialustana.hel.fi/aukiolosovellus/tillganglighetsutlatande",
- "ContentLicenseLink": "Innehållslicens CC BY 4.0"
+ "ContentLicenseLink": "Innehållslicens CC BY 4.0",
+ "CookiesLink": "Cookie -inställningar"
}
-}
\ No newline at end of file
+}
diff --git a/src/pages/CookieManagement.tsx b/src/pages/CookieManagement.tsx
new file mode 100644
index 000000000..a8b6b8a1a
--- /dev/null
+++ b/src/pages/CookieManagement.tsx
@@ -0,0 +1,19 @@
+/* eslint-disable no-underscore-dangle */
+import React from 'react';
+import { CookiePage } from 'hds-react';
+import { useAppContext } from '../App-context';
+
+import useCookieConsent from '../components/cookie-consent/hooks/useCookieConsent';
+
+const CookieManagement = (): JSX.Element => {
+ const { language, setLanguage } = useAppContext();
+ const { config } = useCookieConsent({
+ language,
+ setLanguage,
+ isModal: false,
+ });
+
+ return ;
+};
+
+export default CookieManagement;
diff --git a/src/setupTests.ts b/src/setupTests.ts
index 68945fd37..b86f28476 100644
--- a/src/setupTests.ts
+++ b/src/setupTests.ts
@@ -18,3 +18,16 @@ console.error = (msg: any, ...optionalParams: any[]) => {
originalError(msg, ...optionalParams)
);
};
+
+window.ENV = {
+ REACT_APP_VERSION: '',
+ API_URL: '',
+ USE_AXE: '',
+ SENTRY_DSN: '',
+ SENTRY_ENV: '',
+ FEEDBACK_EMAILS: '',
+ MATOMO_SRC_URL: '',
+ MATOMO_URL_BASE: 'test',
+ MATOMO_SITE_ID: 'test123',
+ MATOMO_ENABLED: false,
+};
diff --git a/yarn.lock b/yarn.lock
index d1949c5d1..19c48642f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5786,16 +5786,36 @@ dot-prop@^5.2.0:
dependencies:
is-obj "^2.0.0"
+dotenv-cli@^7.3.0:
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-7.3.0.tgz#21e33e7944713001677658d68856063968edfbd2"
+ integrity sha512-314CA4TyK34YEJ6ntBf80eUY+t1XaFLyem1k9P0sX1gn30qThZ5qZr/ZwE318gEnzyYP9yj9HJk6SqwE0upkfw==
+ dependencies:
+ cross-spawn "^7.0.3"
+ dotenv "^16.3.0"
+ dotenv-expand "^10.0.0"
+ minimist "^1.2.6"
+
dotenv-expand@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
+dotenv-expand@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37"
+ integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==
+
dotenv@8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
+dotenv@^16.3.0:
+ version "16.3.1"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
+ integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
+
downshift@6.0.6:
version "6.0.6"
resolved "https://registry.yarnpkg.com/downshift/-/downshift-6.0.6.tgz#82aee8e2e260d7ad99df8a0969bd002dd523abe8"