Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/workspaces/BrowserExtensionBanner.tsx
2500 views
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, useMemo, useState } from "react";
8
import UAParser from "ua-parser-js";
9
import { useUserLoader } from "../hooks/use-user-loader";
10
import { useAuthProviderDescriptions } from "../data/auth-providers/auth-provider-descriptions-query";
11
import { useFeatureFlag } from "../data/featureflag-query";
12
import { trackEvent } from "../Analytics";
13
14
import bitbucketButton from "../images/browser-extension/bitbucket.webp";
15
import githubButton from "../images/browser-extension/github.webp";
16
import gitlabButton from "../images/browser-extension/gitlab.webp";
17
import azuredevopsButton from "../images/browser-extension/azure-devops.webp";
18
import { disjunctScmProviders, getDeduplicatedScmProviders } from "../utils";
19
20
const browserExtensionImages = {
21
Bitbucket: bitbucketButton,
22
GitHub: githubButton,
23
GitLab: gitlabButton,
24
"Azure DevOps": azuredevopsButton,
25
} as const;
26
27
type BrowserOption = {
28
type: "firefox" | "chromium";
29
aliases?: string[];
30
url: string;
31
};
32
33
const installationOptions: BrowserOption[] = [
34
{
35
type: "firefox",
36
aliases: ["firefox"],
37
url: "https://addons.mozilla.org/en-US/firefox/addon/gitpod/",
38
},
39
{
40
type: "chromium",
41
aliases: ["chrome", "edge", "brave", "chromium", "vivaldi", "opera"],
42
url: "https://chrome.google.com/webstore/detail/gitpod-always-ready-to-co/dodmmooeoklaejobgleioelladacbeki",
43
},
44
];
45
46
/**
47
* Determines whether the extension has been able to access the current site in the past month. If it hasn't, it's most likely not installed or misconfigured
48
*/
49
const wasRecentlySeenActive = (): boolean => {
50
const lastSeen = localStorage.getItem("extension-last-seen-active");
51
if (!lastSeen) {
52
return false;
53
}
54
55
const threshold = 30 * 24 * 60 * 60 * 1_000; // 1 month
56
return Date.now() - new Date(lastSeen).getTime() < threshold;
57
};
58
59
export function BrowserExtensionBanner() {
60
const { user } = useUserLoader();
61
const { data: authProviderDescriptions } = useAuthProviderDescriptions();
62
63
const usedProviders = useMemo(() => {
64
if (!user || !authProviderDescriptions) return;
65
66
return getDeduplicatedScmProviders(user, authProviderDescriptions);
67
}, [user, authProviderDescriptions]);
68
69
const scmProviderString = useMemo(() => usedProviders && disjunctScmProviders(usedProviders), [usedProviders]);
70
71
const parser = useMemo(() => new UAParser(), []);
72
const browserName = useMemo(() => parser.getBrowser().name?.toLowerCase(), [parser]);
73
74
const [isVisible, setIsVisible] = useState<boolean | null>(null); // null is used to indicate an initial loading state
75
const isFeatureFlagEnabled = useFeatureFlag("showBrowserExtensionPromotion");
76
77
useEffect(() => {
78
const targetElement = document.querySelector(`meta[name="extension-active"]`);
79
if (!targetElement) {
80
return;
81
}
82
83
if (targetElement.getAttribute("content") === "true") {
84
setIsVisible(false);
85
return;
86
}
87
88
const observer = new MutationObserver(() => {
89
setIsVisible(!targetElement.getAttribute("content"));
90
});
91
92
observer.observe(targetElement, {
93
attributes: true,
94
attributeFilter: ["content"],
95
});
96
97
return () => {
98
observer.disconnect();
99
};
100
}, []);
101
102
useEffect(() => {
103
// If the visibility state has already been set, don't override it
104
if (isVisible !== null) {
105
return;
106
}
107
108
const installedOrDismissed =
109
wasRecentlySeenActive() || localStorage.getItem("browser-extension-banner-dismissed");
110
111
setIsVisible(!installedOrDismissed);
112
}, [isVisible]);
113
114
// const handleClose = () => {
115
// let persistSuccess = true;
116
// try {
117
// localStorage.setItem("browser-extension-banner-dismissed", "true");
118
// } catch (e) {
119
// persistSuccess = false;
120
// } finally {
121
// setIsVisible(false);
122
// trackEvent("coachmark_dismissed", {
123
// name: "browser_extension_promotion",
124
// success: persistSuccess,
125
// });
126
// }
127
// };
128
129
const browserOption =
130
browserName &&
131
Object.values(installationOptions).find((opt) => opt.aliases && opt.aliases.includes(browserName));
132
133
const handleClick = useCallback(
134
(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
135
if (!browserOption) return;
136
137
event.preventDefault();
138
139
trackEvent("browser_extension_promotion_interaction", {
140
action: browserOption.type === "chromium" ? "chrome_navigation" : "firefox_navigation",
141
});
142
143
window.open(browserOption.url, "_blank");
144
},
145
[browserOption],
146
);
147
148
if (!isVisible || !browserName || !isFeatureFlagEnabled) {
149
return null;
150
}
151
152
if (!scmProviderString || !usedProviders?.length) {
153
return null;
154
}
155
156
if (!browserOption) {
157
return null;
158
}
159
160
return (
161
<section className="flex justify-center mt-24 mx-4">
162
<div className="sm:flex justify-between border-pk-border-light border-2 rounded-xl hidden max-w-xl mt-4">
163
<div className="flex flex-col gap-1 py-5 pl-6 pr-8 justify-center">
164
<span className="text-lg font-semibold text-pk-content-secondary">
165
Open from {scmProviderString}
166
</span>
167
<span className="text-sm">
168
<a
169
href={browserOption.url}
170
target="_blank"
171
onClick={handleClick}
172
className="gp-link"
173
rel="noreferrer"
174
>
175
Install the Gitpod extension
176
</a>{" "}
177
to launch workspaces from {scmProviderString}.
178
</span>
179
</div>
180
<img
181
alt="A button that says Gitpod"
182
src={browserExtensionImages[usedProviders.at(0)!]}
183
className="w-32 h-fit self-end mb-4 mr-8"
184
/>
185
{/* <Button variant={"ghost"} onClick={handleClose} className="ml-3 self-start hover:bg-transparent">
186
<span className="sr-only">Close</span>
187
<XSvg className={cn("w-3 h-4 dark:text-white text-gray-700")} />
188
</Button> */}
189
</div>
190
</section>
191
);
192
}
193
194