Skip to content

Commit 554c5da

Browse files
committed
Privacy policy update alert
1 parent d4e5e0a commit 554c5da

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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 utc from "dayjs/plugin/utc";
11+
import timezone from "dayjs/plugin/timezone";
12+
import advancedFormat from "dayjs/plugin/advancedFormat";
13+
import { useUserLoader } from "./hooks/use-user-loader";
14+
15+
const KEY_APP_DISMISSED_NOTIFICATIONS = "gitpod-app-notifications-dismissed";
16+
const PRIVACY_POLICY_LAST_UPDATED = "09/25/2023";
17+
18+
interface Notification {
19+
id: string;
20+
type: AlertType;
21+
message: JSX.Element;
22+
preventDismiss?: boolean;
23+
onClose?: () => void;
24+
}
25+
26+
dayjs.extend(utc);
27+
dayjs.extend(timezone);
28+
dayjs.extend(advancedFormat);
29+
30+
export function localizedTime(dateStr: string): JSX.Element {
31+
const formatted = dayjs.utc(dateStr).local().format("dddd, MMM. D, HH:mm (z)");
32+
return <time dateTime={dateStr}>{formatted}</time>;
33+
}
34+
35+
function formatDate(dateString: string): JSX.Element {
36+
const formatted = dayjs.utc(dateString).local().format("LL");
37+
return <time dateTime={dateString}>{formatted}</time>;
38+
}
39+
40+
const UPDATED_PRIVACY_POLICY: Notification = {
41+
id: "privacy-policy-update",
42+
type: "info",
43+
preventDismiss: true,
44+
onClose: () => {
45+
console.error("Well... happy for you");
46+
},
47+
message: (
48+
<span className="text-md">
49+
We've updated the Gitpod Privacy Policy on{" "}
50+
<span className="font-semibold">{formatDate(PRIVACY_POLICY_LAST_UPDATED)}</span>.{" "}
51+
<a className="gp-link" href="https://www.gitpod.io/privacy" target="_blank" rel="noreferrer">
52+
Review Privacy Policy
53+
</a>
54+
</span>
55+
),
56+
};
57+
58+
export function AppNotifications() {
59+
const [topNotification, setTopNotification] = useState<Notification | undefined>(undefined);
60+
const { user, loading } = useUserLoader();
61+
62+
useEffect(() => {
63+
const notifications = [];
64+
if (!loading && user?.additionalData) {
65+
if (new Date(PRIVACY_POLICY_LAST_UPDATED) > new Date(user.additionalData.acceptedPrivacyPoliceDate)) {
66+
notifications.push(UPDATED_PRIVACY_POLICY);
67+
}
68+
}
69+
70+
const dismissedNotifications = getDismissedNotifications();
71+
const topNotification = notifications.find((n) => !dismissedNotifications.includes(n.id));
72+
setTopNotification(topNotification);
73+
}, [loading, setTopNotification, user]);
74+
75+
const dismissNotification = useCallback(() => {
76+
if (!topNotification) {
77+
return;
78+
}
79+
80+
const dismissedNotifications = getDismissedNotifications();
81+
dismissedNotifications.push(topNotification.id);
82+
setDismissedNotifications(dismissedNotifications);
83+
setTopNotification(undefined);
84+
}, [topNotification, setTopNotification]);
85+
86+
if (!topNotification) {
87+
return <></>;
88+
}
89+
90+
return (
91+
<div className="app-container pt-2">
92+
<Alert
93+
type={topNotification.type}
94+
closable={true}
95+
onClose={() => {
96+
if (!topNotification.preventDismiss) {
97+
dismissNotification();
98+
} else {
99+
if (topNotification.onClose) {
100+
topNotification.onClose();
101+
}
102+
}
103+
}}
104+
showIcon={true}
105+
className="flex rounded mb-2 w-full"
106+
>
107+
<span>{topNotification.message}</span>
108+
</Alert>
109+
</div>
110+
);
111+
}
112+
113+
function getDismissedNotifications(): string[] {
114+
try {
115+
const str = window.localStorage.getItem(KEY_APP_DISMISSED_NOTIFICATIONS);
116+
const parsed = JSON.parse(str || "[]");
117+
if (!Array.isArray(parsed)) {
118+
window.localStorage.removeItem(KEY_APP_DISMISSED_NOTIFICATIONS);
119+
return [];
120+
}
121+
return parsed;
122+
} catch (err) {
123+
console.debug("Failed to parse dismissed notifications", err);
124+
window.localStorage.removeItem(KEY_APP_DISMISSED_NOTIFICATIONS);
125+
return [];
126+
}
127+
}
128+
129+
function setDismissedNotifications(ids: string[]) {
130+
try {
131+
window.localStorage.setItem(KEY_APP_DISMISSED_NOTIFICATIONS, JSON.stringify(ids));
132+
} catch (err) {
133+
console.debug("Failed to set dismissed notifications", err);
134+
window.localStorage.removeItem(KEY_APP_DISMISSED_NOTIFICATIONS);
135+
}
136+
}

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

4344
const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ "../workspaces/Workspaces"));
4445
const Account = React.lazy(() => import(/* webpackPrefetch: true */ "../user-settings/Account"));
@@ -121,6 +122,7 @@ export const AppRoutes = () => {
121122
<Route>
122123
<div className="container">
123124
<Menu />
125+
<AppNotifications />
124126
<Switch>
125127
<Route path="/new" exact component={CreateWorkspacePage} />
126128
<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
@@ -301,6 +301,8 @@ export namespace AdditionalUserData {
301301
export interface ProfileDetails {
302302
// when was the last time the user updated their profile information or has been nudged to do so.
303303
lastUpdatedDetailsNudge?: string;
304+
// when was the last time the user has accepted our privacy policy
305+
acceptedPrivacyPoliceDate?: string;
304306
// the user's company name
305307
companyName?: string;
306308
// the user's email

0 commit comments

Comments
 (0)