Skip to content

Commit

Permalink
Add cookie consent dialog and user setting (#3301)
Browse files Browse the repository at this point in the history
"C is for cookie, that's good enough for me" - Cookie Monster
  • Loading branch information
imnasnainaec authored Aug 26, 2024
1 parent a683036 commit bfc9230
Show file tree
Hide file tree
Showing 14 changed files with 159 additions and 13 deletions.
3 changes: 2 additions & 1 deletion docs/user_guide/docs/account.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ You can add or update your:
- name;
- phone number;
- email address;
- user-interface language.
- user-interface language;
- analytics consent.

!!! note "Note"

Expand Down
8 changes: 7 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@
"redux-thunk": "^2.4.0",
"ts-key-enum": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.11.0"
"validator": "^13.11.0",
"vanilla-cookieconsent": "^3.0.1"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
Expand Down
5 changes: 3 additions & 2 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
<meta name="theme-color" content="#000000" />
<script src="%PUBLIC_URL%/scripts/release.js"></script>
<script src="%PUBLIC_URL%/scripts/config.js"></script>
<!-- Add segment.com analytics as described (https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/quickstart/#step-2-copy-the-segment-snippet)
<!-- Add segment.com analytics per https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/quickstart/#step-2-copy-the-segment-snippet
with modifications to read writeKey from window.runtimeConfig if it exists. -->
<script>
<!-- Control analytics per https://cookieconsent.orestbida.com/advanced/manage-scripts.html#how-to-block-manage-a-script-tag -->
<script data-category="analytics" type="text/plain">
!(function () {
var writeKey;
if (
Expand Down
12 changes: 12 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,18 @@
}
},
"userSettings": {
"analyticsConsent": {
"button": "Change consent",
"consentModal": {
"acceptAllBtn": "Yes, allow analytics cookies",
"acceptNecessaryBtn": "No, reject analytics cookies",
"description": "The Combine stores basic info about your current session on your device. This info is necessary and isn't shared with anybody. The Combine also uses analytics cookies, which are only for us to fix bugs and compile anonymized statistics. Do you consent to our usage of analytics cookies?",
"title": "Cookies on The Combine"
},
"consentNo": "You have not consented to our use of analytics cookies.",
"consentYes": "You have consented to our use of analytics cookies.",
"title": "Analytics cookies"
},
"contact": "Contact info",
"glossSuggestion": "Gloss spelling suggestions",
"glossSuggestionHint": "In Data Entry, give spelling suggestions for the Gloss being typed.",
Expand Down
2 changes: 2 additions & 0 deletions src/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RouterProvider } from "react-router-dom";

import AnnouncementBanner from "components/AnnouncementBanner";
import UpperRightToastContainer from "components/Toast/UpperRightToastContainer";
import CookieConsent from "cookies/CookieConsent";
import router from "router/browserRouter";

/**
Expand All @@ -12,6 +13,7 @@ export default function App(): ReactElement {
return (
<div className="App">
<Suspense fallback={<div />}>
<CookieConsent />
<AnnouncementBanner />
<UpperRightToastContainer />
<RouterProvider router={router} />
Expand Down
12 changes: 7 additions & 5 deletions src/components/Login/Redux/LoginActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
setSignupFailureAction,
setSignupSuccessAction,
} from "components/Login/Redux/LoginReducer";
import { type StoreStateDispatch } from "rootRedux/types";
import { type StoreState, type StoreStateDispatch } from "rootRedux/types";
import router from "router/browserRouter";
import { Path } from "types/path";
import { newUser } from "types/user";
Expand Down Expand Up @@ -45,15 +45,17 @@ export function signupSuccess(): PayloadAction {
// Dispatch Functions

export function asyncLogIn(username: string, password: string) {
return async (dispatch: StoreStateDispatch) => {
return async (dispatch: StoreStateDispatch, getState: () => StoreState) => {
dispatch(loginAttempt(username));
await backend
.authenticateUser(username, password)
.then(async (user) => {
dispatch(loginSuccess());
// hash the user name and use it in analytics.identify
const analyticsId = Hex.stringify(sha256(user.id));
analytics.identify(analyticsId);
if (getState().analyticsState.consent) {
// hash the user name and use it in analytics.identify
const analyticsId = Hex.stringify(sha256(user.id));
analytics.identify(analyticsId);
}
router.navigate(Path.ProjScreen);
})
.catch((err) =>
Expand Down
35 changes: 34 additions & 1 deletion src/components/UserSettings/UserSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ import {
import { enqueueSnackbar } from "notistack";
import { FormEvent, Fragment, ReactElement, useState } from "react";
import { useTranslation } from "react-i18next";
import { show } from "vanilla-cookieconsent";

import { AutocompleteSetting, User } from "api/models";
import { isEmailTaken, updateUser } from "backend";
import { getAvatar, getCurrentUser } from "backend/localStorage";
import { asyncLoadSemanticDomains } from "components/Project/ProjectActions";
import ClickableAvatar from "components/UserSettings/ClickableAvatar";
import { updateLangFromUser } from "i18n";
import { useAppDispatch } from "rootRedux/hooks";
import { useAppDispatch, useAppSelector } from "rootRedux/hooks";
import { StoreState } from "rootRedux/types";
import theme from "types/theme";
import { uiWritingSystems } from "types/writingSystem";

Expand All @@ -30,6 +32,7 @@ import { uiWritingSystems } from "types/writingSystem";
const punycode = require("punycode/");

export enum UserSettingsIds {
ButtonChangeConsent = "user-settings-change-consent",
ButtonSubmit = "user-settings-submit",
FieldEmail = "user-settings-email",
FieldName = "user-settings-name",
Expand All @@ -55,6 +58,10 @@ export function UserSettings(props: {
}): ReactElement {
const dispatch = useAppDispatch();

const analyticsConsent = useAppSelector(
(state: StoreState) => state.analyticsState.consent
);

const [name, setName] = useState(props.user.name);
const [phone, setPhone] = useState(props.user.phone);
const [email, setEmail] = useState(props.user.email);
Expand Down Expand Up @@ -269,6 +276,32 @@ export function UserSettings(props: {
</Grid>
</Grid>

<Grid item container spacing={2}>
<Grid item xs={12}>
<Typography variant="h6">
{t("userSettings.analyticsConsent.title")}
</Typography>
</Grid>

<Grid item>
<Typography>
{t(
analyticsConsent
? "userSettings.analyticsConsent.consentYes"
: "userSettings.analyticsConsent.consentNo"
)}
</Typography>
<Button
data-testid={UserSettingsIds.ButtonChangeConsent}
id={UserSettingsIds.ButtonChangeConsent}
onClick={() => show(true)}
variant="outlined"
>
{t("userSettings.analyticsConsent.button")}
</Button>
</Grid>
</Grid>

<Grid item container justifyContent="flex-end">
<Button
data-testid={UserSettingsIds.ButtonSubmit}
Expand Down
1 change: 1 addition & 0 deletions src/components/UserSettings/tests/UserSettings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jest.mock("components/Project/ProjectActions", () => ({
}));
jest.mock("rootRedux/hooks", () => ({
useAppDispatch: () => jest.fn(),
useAppSelector: () => jest.fn(),
}));

// Mock "i18n", else `thrown: "Error: Error: connect ECONNREFUSED ::1:80 [...]`
Expand Down
10 changes: 10 additions & 0 deletions src/cookies/CookieConsent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Fragment, type ReactElement } from "react";

import useCookieConsent from "cookies/useCookieConsent";

/** Empty component for running useCookieConsent within a <Suspense>,
* because it depends on i18n localization loading first. */
export default function CookieConsent(): ReactElement {
useCookieConsent();
return <Fragment />;
}
12 changes: 12 additions & 0 deletions src/cookies/cc.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#cc-main {
--primary: #1e88e5; /* themeColors.primary: blue[600] */
--dark-shade: #0d47a1; /* themeColors.darkShade: blue[900] */

--cc-btn-primary-bg: var(--primary);
--cc-btn-primary-border-color: var(--primary);
--cc-btn-primary-hover-bg: var(--dark-shade);
--cc-btn-primary-hover-border-color: var(--dark-shade);
--cc-font-family: "Noto Sans", "Open Sans", Roboto, Helvetica, Arial, sans-serif;
--cc-primary-color: var(--primary);
--cc-secondary-color: #000000
}
55 changes: 55 additions & 0 deletions src/cookies/useCookieConsent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { eraseCookies, run } from "vanilla-cookieconsent";

import "vanilla-cookieconsent/dist/cookieconsent.css";
import "cookies/cc.css";

import { useAppDispatch } from "rootRedux/hooks";
import { updateConsent } from "types/Redux/analytics";

export default function useCookieConsent(): void {
const dispatch = useAppDispatch();
const { t } = useTranslation();

const updateAnalytics = useCallback(
(param: { cookie: CookieConsent.CookieValue }): void => {
console.info("C is for Cookie...");
dispatch(updateConsent());
if (!param.cookie.categories.includes("analytics")) {
eraseCookies(/^(?!cookie_consent$)/); // Only keep cookie with name "cookie_consent"
}
},
[dispatch]
);

useEffect(() => {
run({
categories: { analytics: {}, necessary: {} },
cookie: { expiresAfterDays: 365, name: "cookie_consent" },
guiOptions: { consentModal: { layout: "bar inline" } },
language: {
default: "i18n",
translations: {
i18n: {
consentModal: {
acceptAllBtn: t(
"userSettings.analyticsConsent.consentModal.acceptAllBtn"
),
acceptNecessaryBtn: t(
"userSettings.analyticsConsent.consentModal.acceptNecessaryBtn"
),
description: t(
"userSettings.analyticsConsent.consentModal.description"
),
title: t("userSettings.analyticsConsent.consentModal.title"),
},
preferencesModal: { sections: [] },
},
},
},
onChange: updateAnalytics,
onFirstConsent: updateAnalytics,
}).then(() => dispatch(updateConsent()));
}, [dispatch, t, updateAnalytics]);
}
12 changes: 10 additions & 2 deletions src/types/Redux/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { getUserPreferences } from "vanilla-cookieconsent";

import { StoreActionTypes } from "rootRedux/actions";
import { defaultState } from "types/Redux/analyticsReduxTypes";
Expand All @@ -8,23 +9,30 @@ const analyticsSlice = createSlice({
initialState: defaultState,
reducers: {
changePageAction: (state, action) => {
if (action.payload !== state.currentPage) {
if (state.consent && action.payload !== state.currentPage) {
analytics.track("navigate", {
destination: action.payload,
source: state.currentPage,
});
}
state.currentPage = action.payload;
},
updateConsentAction: (state) => {
state.consent = getUserPreferences().acceptType === "all";
},
},
extraReducers: (builder) =>
builder.addCase(StoreActionTypes.RESET, () => defaultState),
});

const { changePageAction } = analyticsSlice.actions;
const { changePageAction, updateConsentAction } = analyticsSlice.actions;

export default analyticsSlice.reducer;

export function changePage(newPage: string): PayloadAction {
return changePageAction(newPage);
}

export function updateConsent(): PayloadAction {
return updateConsentAction();
}
2 changes: 2 additions & 0 deletions src/types/Redux/analyticsReduxTypes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export interface AnalyticsState {
consent: boolean;
currentPage: string;
}

export const defaultState: AnalyticsState = {
consent: false,
currentPage: "",
};

0 comments on commit bfc9230

Please sign in to comment.