Skip to content

Commit 36071e6

Browse files
Privacy policy update alert (#18852)
* Privacy policy update alert * Update user * - * Update copy * Fix up styles * End with a period * Update copy * donotmerge: disable auto-acceptance for testing purposes * Address code review Co-authored-by: Brad Harris <[email protected]> * Remove unused imports * Update privacy policy effective date * Remove unused type --------- Co-authored-by: Brad Harris <[email protected]>
1 parent f873b26 commit 36071e6

File tree

4 files changed

+134
-0
lines changed

4 files changed

+134
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { useCallback, useEffect, useState } from "react";
8+
import Alert, { AlertType } from "./components/Alert";
9+
import dayjs from "dayjs";
10+
import { useUserLoader } from "./hooks/use-user-loader";
11+
import { getGitpodService } from "./service/service";
12+
import deepMerge from "deepmerge";
13+
14+
const KEY_APP_DISMISSED_NOTIFICATIONS = "gitpod-app-notifications-dismissed";
15+
const PRIVACY_POLICY_LAST_UPDATED = "2023-10-17";
16+
17+
interface Notification {
18+
id: string;
19+
type: AlertType;
20+
message: JSX.Element;
21+
preventDismiss?: boolean;
22+
onClose?: () => void;
23+
}
24+
25+
const UPDATED_PRIVACY_POLICY: Notification = {
26+
id: "privacy-policy-update",
27+
type: "info",
28+
preventDismiss: true,
29+
onClose: async () => {
30+
const userUpdates = { additionalData: { profile: { acceptedPrivacyPolicyDate: dayjs().toISOString() } } };
31+
const previousUser = await getGitpodService().server.getLoggedInUser();
32+
await getGitpodService().server.updateLoggedInUser(deepMerge(previousUser, userUpdates));
33+
},
34+
message: (
35+
<span className="text-md">
36+
We've updated our Privacy Policy. You can review it{" "}
37+
<a className="gp-link" href="https://www.gitpod.io/privacy" target="_blank" rel="noreferrer">
38+
here
39+
</a>
40+
.
41+
</span>
42+
),
43+
};
44+
45+
export function AppNotifications() {
46+
const [topNotification, setTopNotification] = useState<Notification | undefined>(undefined);
47+
const { user, loading } = useUserLoader();
48+
49+
useEffect(() => {
50+
const notifications = [];
51+
if (!loading && user?.additionalData?.profile) {
52+
if (
53+
!user.additionalData.profile.acceptedPrivacyPolicyDate ||
54+
new Date(PRIVACY_POLICY_LAST_UPDATED) > new Date(user.additionalData.profile?.acceptedPrivacyPolicyDate)
55+
) {
56+
notifications.push(UPDATED_PRIVACY_POLICY);
57+
}
58+
}
59+
60+
const dismissedNotifications = getDismissedNotifications();
61+
const topNotification = notifications.find((n) => !dismissedNotifications.includes(n.id));
62+
setTopNotification(topNotification);
63+
}, [loading, setTopNotification, user]);
64+
65+
const dismissNotification = useCallback(() => {
66+
if (!topNotification) {
67+
return;
68+
}
69+
70+
const dismissedNotifications = getDismissedNotifications();
71+
dismissedNotifications.push(topNotification.id);
72+
setDismissedNotifications(dismissedNotifications);
73+
setTopNotification(undefined);
74+
}, [topNotification, setTopNotification]);
75+
76+
if (!topNotification) {
77+
return <></>;
78+
}
79+
80+
return (
81+
<div className="app-container pt-2">
82+
<Alert
83+
type={topNotification.type}
84+
closable={true}
85+
onClose={() => {
86+
if (!topNotification.preventDismiss) {
87+
dismissNotification();
88+
} else {
89+
if (topNotification.onClose) {
90+
topNotification.onClose();
91+
}
92+
}
93+
}}
94+
showIcon={true}
95+
className="flex rounded mb-2 w-full"
96+
>
97+
<span>{topNotification.message}</span>
98+
</Alert>
99+
</div>
100+
);
101+
}
102+
103+
function getDismissedNotifications(): string[] {
104+
try {
105+
const str = window.localStorage.getItem(KEY_APP_DISMISSED_NOTIFICATIONS);
106+
const parsed = JSON.parse(str || "[]");
107+
if (!Array.isArray(parsed)) {
108+
window.localStorage.removeItem(KEY_APP_DISMISSED_NOTIFICATIONS);
109+
return [];
110+
}
111+
return parsed;
112+
} catch (err) {
113+
console.debug("Failed to parse dismissed notifications", err);
114+
window.localStorage.removeItem(KEY_APP_DISMISSED_NOTIFICATIONS);
115+
return [];
116+
}
117+
}
118+
119+
function setDismissedNotifications(ids: string[]) {
120+
try {
121+
window.localStorage.setItem(KEY_APP_DISMISSED_NOTIFICATIONS, JSON.stringify(ids));
122+
} catch (err) {
123+
console.debug("Failed to set dismissed notifications", err);
124+
window.localStorage.removeItem(KEY_APP_DISMISSED_NOTIFICATIONS);
125+
}
126+
}

components/dashboard/src/app/AppRoutes.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import PersonalAccessTokenCreateView from "../user-settings/PersonalAccessTokens
3939
import { CreateWorkspacePage } from "../workspaces/CreateWorkspacePage";
4040
import { WebsocketClients } from "./WebsocketClients";
4141
import { BlockedEmailDomains } from "../admin/BlockedEmailDomains";
42+
import { AppNotifications } from "../AppNotifications";
4243
import { useFeatureFlag } from "../data/featureflag-query";
4344

4445
const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ "../workspaces/Workspaces"));
@@ -127,6 +128,7 @@ export const AppRoutes = () => {
127128
<Route>
128129
<div className="container">
129130
<Menu />
131+
<AppNotifications />
130132
<Switch>
131133
<Route path="/new" exact component={CreateWorkspacePage} />
132134
<Route path={projectsPathNew} exact component={NewProject} />

components/gitpod-protocol/src/protocol.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ export namespace AdditionalUserData {
302302
export interface ProfileDetails {
303303
// when was the last time the user updated their profile information or has been nudged to do so.
304304
lastUpdatedDetailsNudge?: string;
305+
// when was the last time the user has accepted our privacy policy
306+
acceptedPrivacyPolicyDate?: string;
305307
// the user's company name
306308
companyName?: string;
307309
// the user's email

components/server/src/user/user-service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export class UserService {
7272
// blocked = if user already blocked OR is not allowed to pass
7373
newUser.blocked = newUser.blocked || !canPass;
7474
}
75+
if (newUser.additionalData) {
76+
// When a user is created, it does not have `additionalData.profile` set, so it's ok to rewrite it here.
77+
newUser.additionalData.profile = { acceptedPrivacyPolicyDate: new Date().toISOString() };
78+
}
7579
}
7680

7781
async findUserById(userId: string, id: string): Promise<User> {

0 commit comments

Comments
 (0)