Skip to content

Commit 8966751

Browse files
authored
Merge pull request #1418 from jetstreamapp/chore/cookie-consent-banner
Add cookie banner for opt-in analytics
2 parents d539c70 + c0e592c commit 8966751

32 files changed

+682
-123
lines changed

apps/api/src/app/controllers/auth.controller.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,20 @@ const callback = createRoute(
385385

386386
if (provider.type === 'oauth') {
387387
// oauth flow
388+
// Validate required cookies exist before calling OAuth library
389+
if (!cookies[pkceCodeVerifier.name]) {
390+
throw new InvalidSession('Missing PKCE code verifier - invalid OAuth flow. Please start the login process again.');
391+
}
392+
393+
// Validate OAuth callback has required parameters
394+
const queryParams = new URLSearchParams(query as Record<string, string>);
395+
if (!queryParams.has('code') && !queryParams.has('error')) {
396+
throw new InvalidParameters('Missing OAuth callback parameters. Please start the login process again.');
397+
}
398+
388399
const { userInfo } = await validateCallback(
389400
provider.provider as OauthProviderType,
390-
new URLSearchParams(query as Record<string, string>),
401+
queryParams,
391402
cookies[pkceCodeVerifier.name],
392403
cookies[nonce.name],
393404
);

apps/api/src/main.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ if (ENV.NODE_ENV === 'production' && !ENV.CI && cluster.isPrimary) {
166166
scriptSrc: [
167167
"'self'",
168168
"'sha256-AS526U4qXJy7/SohgsysWUxi77DtcgSmP0hNfTo6/Hs='", // Google Analytics (Docs)
169-
"'sha256-pOkCIUf8FXwCoKWPXTEJAC2XGbyg3ftSrE+IES4aqEY='", // Google Analytics (Next/React)
170169
"'sha256-7mNBpJaHD4L73RpSf1pEaFD17uW3H/9+P1AYhm+j/Dg='", // Monaco unhandledrejection script
171170
"'sha256-U1ZWk/Nvev4hBoGjgXSP/YN1w4VGTmd4NTYtXEr58xI='", // __IS_BROWSER_EXTENSION__ script
172171
'blob:',

apps/jetstream-e2e/src/setup/global.setup.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@ setup('login and ensure org exists', async ({ page, request }) => {
1818
console.log('Logging in as example user');
1919
const user = ENV.EXAMPLE_USER;
2020

21+
const cookieBanner = page.getByText('We use cookies to improve');
22+
const cookieBannerAcceptButton = page.getByRole('button', { name: 'Accept' });
23+
2124
await page.goto(baseApiURL);
25+
26+
if (await cookieBanner.isVisible()) {
27+
await cookieBannerAcceptButton.click();
28+
} else {
29+
console.log('Element not visible, skipping click.');
30+
}
31+
2232
await page.getByRole('link', { name: 'Log in' }).click();
2333
await page.getByLabel('Email Address').click();
2434
await page.getByLabel('Email Address').fill(user.email);

apps/jetstream/index.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@
3131
<link rel="icon" type="image/png" sizes="32x32" href="/assets/images/favicon-32x32.png" />
3232
<link rel="icon" type="image/png" sizes="96x96" href="/assets/images/favicon-96x96.png" />
3333
<link rel="icon" type="image/png" sizes="16x16" href="/assets/images/favicon-16x16.png" />
34-
<script async src="https://www.googletagmanager.com/gtag/js?id=G-GZJ9QQTK44"></script>
35-
<!-- prettier-ignore -->
36-
<script>window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}if(window.location.hostname!=='localhost'){gtag('js', new Date());gtag('config', 'G-GZJ9QQTK44');}</script>
3734
<!-- Monaco throws error when hovering on a completion -->
3835
<!-- any changes need to be adjusted in CSP hash -->
3936
<script>

apps/jetstream/src/app/components/core/AppInitializer.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useObservable, useRollbar } from '@jetstream/shared/ui-utils';
66
import { Announcement, SalesforceOrgUi } from '@jetstream/types';
77
import { useAmplitude } from '@jetstream/ui-core';
88
import { fromAppState } from '@jetstream/ui/app-state';
9+
import { CookieConsentBanner, useConditionalGoogleAnalytics } from '@jetstream/ui/cookie-consent-banner';
910
import { initDexieDb } from '@jetstream/ui/db';
1011
import { AxiosResponse } from 'axios';
1112
import { useAtom, useAtomValue } from 'jotai';
@@ -42,6 +43,9 @@ export const AppInitializer: FunctionComponent<AppInitializerProps> = ({ onAnnou
4243
const { version, announcements, appInfo } = useAtomValue(fromAppState.appInfoState);
4344
const [orgs, setOrgs] = useAtom(fromAppState.salesforceOrgsState);
4445
const invalidOrg = useObservable(orgConnectionError$);
46+
const [analytics, setAnalytics] = useAtom(fromAppState.analyticsState);
47+
48+
useConditionalGoogleAnalytics(environment.googleAnalyticsSiteId, analytics === 'accepted');
4549

4650
const recordSyncEntitlementEnabled = ability.can('access', 'RecordSync');
4751
const recordSyncEnabled = recordSyncEntitlementEnabled && userProfile.preferences.recordSyncEnabled;
@@ -88,7 +92,7 @@ APP VERSION ${version}
8892
userProfile: userProfile,
8993
version,
9094
});
91-
useAmplitude();
95+
useAmplitude(analytics !== 'accepted');
9296

9397
useEffect(() => {
9498
if (invalidOrg) {
@@ -137,8 +141,12 @@ APP VERSION ${version}
137141
return () => document.removeEventListener('visibilitychange', handleWindowFocus);
138142
}, [handleWindowFocus]);
139143

140-
// eslint-disable-next-line react/jsx-no-useless-fragment
141-
return <Fragment>{children}</Fragment>;
144+
return (
145+
<Fragment>
146+
<CookieConsentBanner onConsentChange={setAnalytics} />
147+
{children}
148+
</Fragment>
149+
);
142150
};
143151

144152
export default AppInitializer;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { RadioButton, RadioGroup } from '@jetstream/ui';
2+
import { fromAppState } from '@jetstream/ui/app-state';
3+
import { useCookieConsent } from '@jetstream/ui/cookie-consent-banner';
4+
import { useAtom } from 'jotai';
5+
6+
type ConsentValue = 'accepted' | 'rejected' | null;
7+
8+
export const AnalyticsTrackingSetting = () => {
9+
const { acceptAll, rejectAll } = useCookieConsent();
10+
const [analytics, setAnalytics] = useAtom(fromAppState.analyticsState);
11+
12+
function handleChange(value: ConsentValue) {
13+
setAnalytics(value);
14+
if (value === 'accepted') {
15+
acceptAll();
16+
} else {
17+
rejectAll();
18+
}
19+
}
20+
21+
return (
22+
<RadioGroup label="Allow Analytics Tracking" isButtonGroup>
23+
<RadioButton
24+
id="cookie-consent-accept"
25+
name="cookie-analytics-consent"
26+
label="Allow"
27+
value="accepted"
28+
checked={analytics === 'accepted'}
29+
onChange={(value) => handleChange(value as ConsentValue)}
30+
/>
31+
<RadioButton
32+
id="cookie-consent-reject"
33+
name="cookie-analytics-consent"
34+
label="Disallow"
35+
value="rejected"
36+
checked={analytics !== 'accepted'}
37+
onChange={(value) => handleChange(value as ConsentValue)}
38+
/>
39+
</RadioGroup>
40+
);
41+
};

apps/jetstream/src/app/components/settings/Settings.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { dexieDataSync, recentHistoryItemsDb } from '@jetstream/ui/db';
2121
import { useAtom, useAtomValue } from 'jotai';
2222
import localforage from 'localforage';
2323
import { useCallback, useEffect, useRef, useState } from 'react';
24+
import { AnalyticsTrackingSetting } from './AnalyticsTrackingSetting';
2425
import LoggerConfig from './LoggerConfig';
2526
import { SettingsDeleteAccount } from './SettingsDeleteAccount';
2627
const HEIGHT_BUFFER = 170;
@@ -255,6 +256,11 @@ export const Settings = () => {
255256
<LoggerConfig />
256257
</div>
257258

259+
<div className="slds-m-top_large">
260+
<h2 className="slds-text-heading_medium slds-m-vertical_small">Analytics</h2>
261+
<AnalyticsTrackingSetting />
262+
</div>
263+
258264
{!userProfile.teamMembership && <SettingsDeleteAccount onDeleteAccount={handleDelete} />}
259265
</div>
260266
)}

apps/jetstream/src/environments/environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
export const environment = {
55
name: 'JetstreamDev',
66
production: false,
7+
googleAnalyticsSiteId: import.meta.env.NX_GOOGLE_ANALYTICS_KEY || '',
78
rollbarClientAccessToken: import.meta.env.NX_PUBLIC_ROLLBAR_KEY,
89
amplitudeToken: import.meta.env.NX_PUBLIC_AMPLITUDE_KEY,
910
STRIPE_PUBLIC_KEY: import.meta.env.NX_PUBLIC_STRIPE_PUBLIC_KEY,

apps/landing/components/HeaderNoNavigation.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @next/next/no-img-element */
21
import Link from 'next/link';
32
import { ROUTES } from '../utils/environment';
43

apps/landing/components/auth/LoginOrSignUp.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ const FormSchema = z.discriminatedUnion('action', [LoginSchema, RegisterSchema])
5555
}
5656
});
5757

58-
type LoginForm = z.infer<typeof LoginSchema>;
5958
type RegisterForm = z.infer<typeof RegisterSchema>;
6059
type Form = z.infer<typeof FormSchema>;
6160

0 commit comments

Comments
 (0)