Skip to content

Commit

Permalink
Add modal for prompting t&c consent
Browse files Browse the repository at this point in the history
This is a really basic consent modal, that is only
closeable by agreeing to the configured terms and
conditions. Users are prompted when first visiting
Tobira.
The consent is saved in local storage, meaning it is
done on a per-device basis rather than per-user.
It gets away with using a simple hashing function
since it does not contain any sensible information.
Users are re-prompted once anything in the T&Cs changes.
  • Loading branch information
owi92 committed Feb 12, 2024
1 parent e8291e5 commit 52986f7
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 3 deletions.
3 changes: 3 additions & 0 deletions frontend/src/layout/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { OperationType } from "relay-runtime";
import { UserData$key } from "../__generated__/UserData.graphql";
import { useNoindexTag } from "../util";
import { screenWidthAtMost } from "@opencast/appkit";
import { InitialConsent } from "../ui/InitialConsent";
import CONFIG from "../config";


export const MAIN_PADDING = 16;
Expand All @@ -31,6 +33,7 @@ export const Root: React.FC<Props> = ({ nav, children }) => {

return (
<Outer disableScrolling={menu.state === "burger"}>
{CONFIG.initialConsent && <InitialConsent />}
<Header hideNavIcon={!navExists} />
{menu.state === "burger" && navExists && (
<BurgerMenu items={navElements} hide={() => menu.close()} />
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/ui/InitialConsent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useRef } from "react";
import CONFIG from "../config";
import { currentRef, useTranslatedConfig } from "../util";
import { Modal, ModalHandle } from "./Modal";
import { Button } from "./Button";
import { TextBlock } from "./Blocks/Text";


export const InitialConsent: React.FC = () => {
const modalRef = useRef<ModalHandle>(null);
const userConsent = localStorage.getItem("userConsent") ?? "";
const { title, text, button } = CONFIG.initialConsent;

// Since this doesn't store any critical information, a simple, insecure hash
// should suffice.
// Source: https://gist.github.com/jlevy/c246006675becc446360a798e2b2d781
const simpleHash = (str: string) => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
}
return (hash >>> 0).toString(36);
};

const hash = simpleHash(CONFIG.initialConsent.toString());

return userConsent !== hash ? (
<Modal
open
ref={modalRef}
title={useTranslatedConfig(title)}
closable={false}
>
<TextBlock content={useTranslatedConfig(text)} />
<Button autoFocus css={{ marginTop: 20 }} onClick={() => {
localStorage.setItem("userConsent", hash);
currentRef(modalRef).close?.();
}}>{useTranslatedConfig(button)}</Button>
</Modal>
) : null;
};

8 changes: 5 additions & 3 deletions frontend/src/ui/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type ModalProps = {
closable?: boolean;
className?: string;
closeOnOutsideClick?: boolean;
open?: boolean;
};

export type ModalHandle = {
Expand All @@ -41,16 +42,17 @@ export const Modal = forwardRef<ModalHandle, PropsWithChildren<ModalProps>>(({
children,
className,
closeOnOutsideClick = false,
open = false,
}, ref) => {
const { t } = useTranslation();
const [isOpen, setOpen] = useState(false);
const [isOpen, setOpen] = useState(open);
const isDark = useColorScheme().scheme === "dark";

useImperativeHandle(ref, () => ({
isOpen: () => isOpen,
open: () => setOpen(true),
close: closable ? (() => setOpen(false)) : undefined,
}), [isOpen, closable]);
close: () => setOpen(false),
}), [isOpen]);

useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
Expand Down

0 comments on commit 52986f7

Please sign in to comment.