Skip to content

Privacy policy update alert #18852

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

Merged
merged 14 commits into from
Oct 17, 2023
Merged
126 changes: 126 additions & 0 deletions components/dashboard/src/AppNotifications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import { useCallback, useEffect, useState } from "react";
import Alert, { AlertType } from "./components/Alert";
import dayjs from "dayjs";
import { useUserLoader } from "./hooks/use-user-loader";
import { getGitpodService } from "./service/service";
import deepMerge from "deepmerge";

const KEY_APP_DISMISSED_NOTIFICATIONS = "gitpod-app-notifications-dismissed";
const PRIVACY_POLICY_LAST_UPDATED = "2023-10-17";

interface Notification {
id: string;
type: AlertType;
message: JSX.Element;
preventDismiss?: boolean;
onClose?: () => void;
}

const UPDATED_PRIVACY_POLICY: Notification = {
id: "privacy-policy-update",
type: "info",
preventDismiss: true,
onClose: async () => {
const userUpdates = { additionalData: { profile: { acceptedPrivacyPolicyDate: dayjs().toISOString() } } };
const previousUser = await getGitpodService().server.getLoggedInUser();
await getGitpodService().server.updateLoggedInUser(deepMerge(previousUser, userUpdates));
},
message: (
<span className="text-md">
We've updated our Privacy Policy. You can review it{" "}
<a className="gp-link" href="https://www.gitpod.io/privacy" target="_blank" rel="noreferrer">
here
</a>
.
</span>
),
};

export function AppNotifications() {
const [topNotification, setTopNotification] = useState<Notification | undefined>(undefined);
const { user, loading } = useUserLoader();

useEffect(() => {
const notifications = [];
if (!loading && user?.additionalData?.profile) {
if (
!user.additionalData.profile.acceptedPrivacyPolicyDate ||
new Date(PRIVACY_POLICY_LAST_UPDATED) > new Date(user.additionalData.profile?.acceptedPrivacyPolicyDate)
) {
notifications.push(UPDATED_PRIVACY_POLICY);
}
}

const dismissedNotifications = getDismissedNotifications();
const topNotification = notifications.find((n) => !dismissedNotifications.includes(n.id));
setTopNotification(topNotification);
}, [loading, setTopNotification, user]);

const dismissNotification = useCallback(() => {
if (!topNotification) {
return;
}

const dismissedNotifications = getDismissedNotifications();
dismissedNotifications.push(topNotification.id);
setDismissedNotifications(dismissedNotifications);
setTopNotification(undefined);
}, [topNotification, setTopNotification]);

if (!topNotification) {
return <></>;
}

return (
<div className="app-container pt-2">
<Alert
type={topNotification.type}
closable={true}
onClose={() => {
if (!topNotification.preventDismiss) {
dismissNotification();
} else {
if (topNotification.onClose) {
topNotification.onClose();
}
}
}}
showIcon={true}
className="flex rounded mb-2 w-full"
>
<span>{topNotification.message}</span>
</Alert>
</div>
);
}

function getDismissedNotifications(): string[] {
try {
const str = window.localStorage.getItem(KEY_APP_DISMISSED_NOTIFICATIONS);
const parsed = JSON.parse(str || "[]");
if (!Array.isArray(parsed)) {
window.localStorage.removeItem(KEY_APP_DISMISSED_NOTIFICATIONS);
return [];
}
return parsed;
} catch (err) {
console.debug("Failed to parse dismissed notifications", err);
window.localStorage.removeItem(KEY_APP_DISMISSED_NOTIFICATIONS);
return [];
}
}

function setDismissedNotifications(ids: string[]) {
try {
window.localStorage.setItem(KEY_APP_DISMISSED_NOTIFICATIONS, JSON.stringify(ids));
} catch (err) {
console.debug("Failed to set dismissed notifications", err);
window.localStorage.removeItem(KEY_APP_DISMISSED_NOTIFICATIONS);
}
}
2 changes: 2 additions & 0 deletions components/dashboard/src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import PersonalAccessTokenCreateView from "../user-settings/PersonalAccessTokens
import { CreateWorkspacePage } from "../workspaces/CreateWorkspacePage";
import { WebsocketClients } from "./WebsocketClients";
import { BlockedEmailDomains } from "../admin/BlockedEmailDomains";
import { AppNotifications } from "../AppNotifications";
import { useFeatureFlag } from "../data/featureflag-query";

const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ "../workspaces/Workspaces"));
Expand Down Expand Up @@ -127,6 +128,7 @@ export const AppRoutes = () => {
<Route>
<div className="container">
<Menu />
<AppNotifications />
<Switch>
<Route path="/new" exact component={CreateWorkspacePage} />
<Route path={projectsPathNew} exact component={NewProject} />
Expand Down
2 changes: 2 additions & 0 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ export namespace AdditionalUserData {
export interface ProfileDetails {
// when was the last time the user updated their profile information or has been nudged to do so.
lastUpdatedDetailsNudge?: string;
// when was the last time the user has accepted our privacy policy
acceptedPrivacyPolicyDate?: string;
// the user's company name
companyName?: string;
// the user's email
Expand Down
4 changes: 4 additions & 0 deletions components/server/src/user/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export class UserService {
// blocked = if user already blocked OR is not allowed to pass
newUser.blocked = newUser.blocked || !canPass;
}
if (newUser.additionalData) {
// When a user is created, it does not have `additionalData.profile` set, so it's ok to rewrite it here.
newUser.additionalData.profile = { acceptedPrivacyPolicyDate: new Date().toISOString() };
}
}

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