Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: gdpr compliance #4070

Merged
merged 58 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
d8e2664
feat: cookie banner gdpr
sshanzel Jan 10, 2025
282367c
feat: gdpr popup
sshanzel Jan 13, 2025
fcbc4c1
feat: yt video placeholder
sshanzel Jan 13, 2025
ff55db4
fix: wrong condition
sshanzel Jan 13, 2025
17dd2a8
test: gdpr banner
sshanzel Jan 13, 2025
eccfc59
test: consent modal
sshanzel Jan 15, 2025
cb76106
refactor: pixel tracking on eu continent
sshanzel Jan 15, 2025
b93217f
fix: build
sshanzel Jan 15, 2025
0d33906
refactor: removal of old banner
sshanzel Jan 15, 2025
50781c9
refactor: naming convention
sshanzel Jan 15, 2025
7fb82b4
fix: test
sshanzel Jan 15, 2025
8ab96f5
test: fix switch tests
sshanzel Jan 15, 2025
0cf53f2
refactor: cookie names
sshanzel Jan 15, 2025
6ff37cc
feat: privacy page
sshanzel Jan 15, 2025
b119af0
fix: lint
sshanzel Jan 15, 2025
4b1f492
fix: profiel options menu
sshanzel Jan 15, 2025
4a0257d
fix: toggle callback
sshanzel Jan 15, 2025
39e324a
fix: lint
sshanzel Jan 15, 2025
6ab3b42
fix: missing description for necessary cookies.
sshanzel Jan 16, 2025
9ccdbd4
fix: keys to save
sshanzel Jan 16, 2025
c264cb0
test: rerender
sshanzel Jan 16, 2025
4f34e4a
Merge branch 'main' into MI-736
sshanzel Jan 16, 2025
bdcecf0
Update packages/shared/src/components/modals/common.tsx
sshanzel Jan 16, 2025
cbc6606
fix: disabled state for necessary option
sshanzel Jan 16, 2025
e2c7621
fix: redirect
sshanzel Jan 16, 2025
919bf71
fix: dev mode
sshanzel Jan 16, 2025
ecd5df3
fix: conditional item from the menu
sshanzel Jan 16, 2025
9d0f806
fix: lint
sshanzel Jan 16, 2025
142bbc6
Merge branch 'main' into MI-736
sshanzel Jan 16, 2025
46375cc
Update packages/shared/src/components/icons/Privacy/filled.svg
sshanzel Jan 16, 2025
b2484e5
refactor: retain banner when gdpr covered
sshanzel Jan 17, 2025
275e88e
fix: build
sshanzel Jan 17, 2025
7e4ec0a
fix: export
sshanzel Jan 17, 2025
18b6515
fix: test
sshanzel Jan 17, 2025
c745d0b
Merge branch 'main' into MI-736
sshanzel Jan 17, 2025
b0c7389
revert: faq section
sshanzel Jan 17, 2025
817b7e7
fix: condition
sshanzel Jan 17, 2025
a436f86
Merge branch 'main' into MI-736
sshanzel Jan 21, 2025
bfdf2c3
fix: lint
sshanzel Jan 21, 2025
a300faf
refactor: gdpr coverage
sshanzel Jan 21, 2025
bd9dc53
fix: no close button for GDPR
sshanzel Jan 21, 2025
3c9c4e5
refactor: cookie banner for all
sshanzel Jan 21, 2025
bf882bf
refactor: compliance
sshanzel Jan 21, 2025
44eb342
fix: is initialized
sshanzel Jan 21, 2025
6e7466f
fix: test functions
sshanzel Jan 21, 2025
d6efb42
fix: import
sshanzel Jan 21, 2025
34afaa7
fix: unused deps
sshanzel Jan 21, 2025
3bd2526
Merge branch 'main' into MI-736
sshanzel Jan 21, 2025
aa9eed3
Merge branch 'main' into MI-736
sshanzel Jan 22, 2025
16a8024
fix: pixel tracking rendering
sshanzel Jan 22, 2025
1e44669
fix: reactiveness of the data
sshanzel Jan 22, 2025
11c5464
Merge branch 'main' into MI-736
sshanzel Jan 22, 2025
adec82d
fix: deps
sshanzel Jan 22, 2025
7062620
refactor: moved to its own hook
sshanzel Jan 22, 2025
ea413b0
fix: unnecessary memoization
sshanzel Jan 22, 2025
b71fe51
fix: build
sshanzel Jan 22, 2025
2214a2b
fix: build
sshanzel Jan 22, 2025
2bcbf48
revert: typography changes
sshanzel Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/shared/src/components/Pixels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { isProduction } from '../lib/constants';
import type { UserExperienceLevel } from '../lib/user';
import { useAuthContext } from '../contexts/AuthContext';
import { fromCDN } from '../lib';
import { GdprConsentKey } from '../hooks/useCookieBanner';
import { useConsentCookie } from '../hooks/useCookieConsent';

const FB_PIXEL_ID = '519268979315924';
const GA_TRACKING_ID = 'G-VTGLXD7QSN';
Expand Down Expand Up @@ -196,15 +198,23 @@ export const logPixelPayment = (
};

export const Pixels = ({ hotjarId }: Partial<HotjarProps>): ReactElement => {
const { user, anonymous } = useAuthContext();
const { cookieExists: acceptedMarketing } = useConsentCookie(
GdprConsentKey.Marketing,
);
const { user, anonymous, isAuthReady, isGdprCovered } = useAuthContext();
const userId = user?.id || anonymous?.id;

const { query } = useRouter();
const instanceId = query?.aiid?.toString();

const props: PixelProps = { userId, instanceId };

if (!isProduction || !userId) {
if (
!isProduction ||
!userId ||
!isAuthReady ||
(isGdprCovered && !acceptedMarketing)
) {
return null;
}

Expand Down
15 changes: 14 additions & 1 deletion packages/shared/src/components/ProfileMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PauseIcon,
EditIcon,
DevPlusIcon,
PrivacyIcon,
} from './icons';
import InteractivePopup, {
InteractivePopupPosition,
Expand Down Expand Up @@ -50,7 +51,7 @@ export default function ProfileMenu({
onClose,
}: ProfileMenuProps): ReactElement {
const { openModal } = useLazyModal();
const { user, logout } = useAuthContext();
const { user, logout, isGdprCovered } = useAuthContext();
const { isActive: isDndActive, setShowDnd } = useDndContext();
const { showPlusSubscription, isPlus, logSubscriptionEvent } =
usePlusSubscription();
Expand Down Expand Up @@ -142,6 +143,17 @@ export default function ProfileMenu({
},
});

if (isGdprCovered) {
list.push({
title: 'Privacy',
buttonProps: {
tag: 'a',
icon: <PrivacyIcon />,
href: `${webappUrl}account/privacy`,
},
});
}

list.push({
title: 'Logout',
buttonProps: {
Expand All @@ -152,6 +164,7 @@ export default function ProfileMenu({

return list.filter(Boolean);
}, [
isGdprCovered,
isDndActive,
isPlus,
logSubscriptionEvent,
Expand Down
44 changes: 44 additions & 0 deletions packages/shared/src/components/accordion/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { ReactElement, ReactNode } from 'react';
import React, { useState } from 'react';
import classNames from 'classnames';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import { ArrowIcon } from '../icons';

interface AccordionProps {
title: ReactNode;
children: ReactNode;
}

export function Accordion({ title, children }: AccordionProps): ReactElement {
const [isOpen, setIsOpen] = useState(false);

return (
<div className="flex w-full flex-col">
<span className="flex w-full flex-row">
{title}
<div className="flex flex-1" />
<Button
type="button"
icon={
<ArrowIcon
className={classNames('transition-transform ease-in-out', {
'rotate-180': !isOpen,
})}
/>
}
size={ButtonSize.XSmall}
variant={ButtonVariant.Tertiary}
onClick={() => setIsOpen(!isOpen)}
/>
</span>
<div
className={classNames(
'flex h-full min-h-0 w-full flex-col overflow-y-hidden break-words transition-[max-height,margin] duration-300 ease-in-out',
isOpen ? 'mt-3 max-h-full' : 'max-h-0',
)}
>
{children}
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions packages/shared/src/components/icons/Privacy/filled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/shared/src/components/icons/Privacy/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { IconProps } from '../../Icon';
import Icon from '../../Icon';
import OutlinedIcon from './outlined.svg';
import FilledIcon from './filled.svg';

export const PrivacyIcon = (props: IconProps): ReactElement => (
<Icon {...props} IconPrimary={OutlinedIcon} IconSecondary={FilledIcon} />
);
9 changes: 9 additions & 0 deletions packages/shared/src/components/icons/Privacy/outlined.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/shared/src/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,4 @@ export * from './ShieldWarning';
export * from './ShieldPlus';
export * from './Sidebar';
export * from './Folder';
export * from './Privacy';
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
screen,
waitFor,
} from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QueryClient } from '@tanstack/react-query';
import React from 'react';
import nock from 'nock';
import SharedBookmarksModal from './SharedBookmarksModal';
Expand All @@ -17,6 +17,7 @@ import {
BOOKMARK_SHARING_QUERY,
} from '../../graphql/bookmarksSharing';
import { waitForNock } from '../../../__tests__/helpers/utilities';
import { TestBootProvider } from '../../../__tests__/helpers/boot';

const onRequestClose = jest.fn();

Expand All @@ -37,13 +38,13 @@ const renderComponent = (mocks: MockedGraphQLResponse[] = []): RenderResult => {

mocks.forEach(mockGraphQL);
return render(
<QueryClientProvider client={client}>
<TestBootProvider client={client}>
<SharedBookmarksModal
isOpen
onRequestClose={onRequestClose}
ariaHideApp={false}
/>
</QueryClientProvider>,
</TestBootProvider>,
);
};

Expand Down
35 changes: 18 additions & 17 deletions packages/shared/src/components/modals/SubmitArticleModal.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { RenderResult } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QueryClient } from '@tanstack/react-query';
import React from 'react';
import nock from 'nock';
import { AuthContextProvider } from '../../contexts/AuthContext';
import type { AnonymousUser, LoggedUser } from '../../lib/user';
import type { MockedGraphQLResponse } from '../../../__tests__/helpers/graphql';
import { mockGraphQL } from '../../../__tests__/helpers/graphql';
Expand All @@ -17,6 +16,7 @@ import user from '../../../__tests__/fixture/loggedUser';
import { NotificationsContextProvider } from '../../contexts/NotificationsContext';
import { waitForNock } from '../../../__tests__/helpers/utilities';
import Toast from '../notifications/Toast';
import { TestBootProvider } from '../../../__tests__/helpers/boot';

const onRequestClose = jest.fn();

Expand All @@ -43,21 +43,22 @@ const renderComponent = (
const client = new QueryClient();
mocks.forEach(mockGraphQL);
return render(
<QueryClientProvider client={client}>
<AuthContextProvider
user={userUpdate}
updateUser={jest.fn()}
tokenRefreshed
getRedirectUri={jest.fn()}
loadingUser={false}
loadedUserFromCache
>
<NotificationsContextProvider>
<Toast />
<SubmitArticleModal isOpen onRequestClose={onRequestClose} />
</NotificationsContextProvider>
</AuthContextProvider>
</QueryClientProvider>,
<TestBootProvider
client={client}
auth={{
user: userUpdate,
updateUser: jest.fn(),
tokenRefreshed: true,
getRedirectUri: jest.fn(),
loadingUser: false,
loadedUserFromCache: true,
}}
>
<NotificationsContextProvider>
<Toast />
<SubmitArticleModal isOpen onRequestClose={onRequestClose} />
</NotificationsContextProvider>
</TestBootProvider>,
);
};

Expand Down
8 changes: 8 additions & 0 deletions packages/shared/src/components/modals/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ const AddToCustomFeedModal = dynamic(
),
);

const CookieConsentModal = dynamic(
() =>
import(
/* webpackChunkName: "cookieConsentModal" */ './user/CookieConsentModal'
),
);

const ReportUserModal = dynamic(
() =>
import(
Expand Down Expand Up @@ -257,6 +264,7 @@ export const modals = {
[LazyModal.ClickbaitShield]: ClickbaitShieldModal,
[LazyModal.MoveBookmark]: MoveBookmarkModal,
[LazyModal.AddToCustomFeed]: AddToCustomFeedModal,
[LazyModal.CookieConsent]: CookieConsentModal,
[LazyModal.ReportUser]: ReportUserModal,
};

Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/components/modals/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export enum LazyModal {
ClickbaitShield = 'clickbaitShield',
MoveBookmark = 'moveBookmark',
AddToCustomFeed = 'addToCustomFeed',
CookieConsent = 'cookieConsent',
ReportUser = 'reportUser',
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { ReactElement } from 'react';
import React, { useState } from 'react';
import { Switch } from '../../fields/Switch';
import type { GdprConsentKey } from '../../../hooks/useCookieBanner';
import { gdprConsentSettings } from '../../../hooks/useCookieBanner';
import {
Typography,
TypographyColor,
TypographyType,
} from '../../typography/Typography';
import { Accordion } from '../../accordion';
import { getCookies } from '../../../lib/cookie';

interface CookieConsentItemProps {
consent: GdprConsentKey;
onToggle?: (value: boolean) => void;
}

const getCookie = (key: GdprConsentKey) => {
const cookies = getCookies([key]);
const disabled = globalThis?.localStorage.getItem(key);

return !!cookies[key] || !disabled;
};

export function CookieConsentItem({
consent,
onToggle,
}: CookieConsentItemProps): ReactElement {
const { title, description, isAlwaysOn } = gdprConsentSettings[consent];
const [isChecked, setIsChecked] = useState<boolean>(
isAlwaysOn ?? getCookie(consent),
);

if (!gdprConsentSettings[consent]) {
return null;
}

const onToggleSwitch = () => {
if (isAlwaysOn) {
return;
}

const value = !isChecked;

setIsChecked(value);

if (onToggle) {
onToggle(value);
}
};

return (
<Accordion
key={consent}
title={
<Switch
disabled={isAlwaysOn}
name={consent}
inputId={consent}
checked={isChecked}
onToggle={onToggleSwitch}
>
{title}
</Switch>
}
>
<Typography
type={TypographyType.Callout}
color={TypographyColor.Tertiary}
>
{description}
</Typography>
</Accordion>
);
}
Loading
Loading