Path: blob/main/components/dashboard/src/workspaces/BrowserExtensionBanner.tsx
2500 views
/**1* Copyright (c) 2023 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { useCallback, useEffect, useMemo, useState } from "react";7import UAParser from "ua-parser-js";8import { useUserLoader } from "../hooks/use-user-loader";9import { useAuthProviderDescriptions } from "../data/auth-providers/auth-provider-descriptions-query";10import { useFeatureFlag } from "../data/featureflag-query";11import { trackEvent } from "../Analytics";1213import bitbucketButton from "../images/browser-extension/bitbucket.webp";14import githubButton from "../images/browser-extension/github.webp";15import gitlabButton from "../images/browser-extension/gitlab.webp";16import azuredevopsButton from "../images/browser-extension/azure-devops.webp";17import { disjunctScmProviders, getDeduplicatedScmProviders } from "../utils";1819const browserExtensionImages = {20Bitbucket: bitbucketButton,21GitHub: githubButton,22GitLab: gitlabButton,23"Azure DevOps": azuredevopsButton,24} as const;2526type BrowserOption = {27type: "firefox" | "chromium";28aliases?: string[];29url: string;30};3132const installationOptions: BrowserOption[] = [33{34type: "firefox",35aliases: ["firefox"],36url: "https://addons.mozilla.org/en-US/firefox/addon/gitpod/",37},38{39type: "chromium",40aliases: ["chrome", "edge", "brave", "chromium", "vivaldi", "opera"],41url: "https://chrome.google.com/webstore/detail/gitpod-always-ready-to-co/dodmmooeoklaejobgleioelladacbeki",42},43];4445/**46* 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 misconfigured47*/48const wasRecentlySeenActive = (): boolean => {49const lastSeen = localStorage.getItem("extension-last-seen-active");50if (!lastSeen) {51return false;52}5354const threshold = 30 * 24 * 60 * 60 * 1_000; // 1 month55return Date.now() - new Date(lastSeen).getTime() < threshold;56};5758export function BrowserExtensionBanner() {59const { user } = useUserLoader();60const { data: authProviderDescriptions } = useAuthProviderDescriptions();6162const usedProviders = useMemo(() => {63if (!user || !authProviderDescriptions) return;6465return getDeduplicatedScmProviders(user, authProviderDescriptions);66}, [user, authProviderDescriptions]);6768const scmProviderString = useMemo(() => usedProviders && disjunctScmProviders(usedProviders), [usedProviders]);6970const parser = useMemo(() => new UAParser(), []);71const browserName = useMemo(() => parser.getBrowser().name?.toLowerCase(), [parser]);7273const [isVisible, setIsVisible] = useState<boolean | null>(null); // null is used to indicate an initial loading state74const isFeatureFlagEnabled = useFeatureFlag("showBrowserExtensionPromotion");7576useEffect(() => {77const targetElement = document.querySelector(`meta[name="extension-active"]`);78if (!targetElement) {79return;80}8182if (targetElement.getAttribute("content") === "true") {83setIsVisible(false);84return;85}8687const observer = new MutationObserver(() => {88setIsVisible(!targetElement.getAttribute("content"));89});9091observer.observe(targetElement, {92attributes: true,93attributeFilter: ["content"],94});9596return () => {97observer.disconnect();98};99}, []);100101useEffect(() => {102// If the visibility state has already been set, don't override it103if (isVisible !== null) {104return;105}106107const installedOrDismissed =108wasRecentlySeenActive() || localStorage.getItem("browser-extension-banner-dismissed");109110setIsVisible(!installedOrDismissed);111}, [isVisible]);112113// const handleClose = () => {114// let persistSuccess = true;115// try {116// localStorage.setItem("browser-extension-banner-dismissed", "true");117// } catch (e) {118// persistSuccess = false;119// } finally {120// setIsVisible(false);121// trackEvent("coachmark_dismissed", {122// name: "browser_extension_promotion",123// success: persistSuccess,124// });125// }126// };127128const browserOption =129browserName &&130Object.values(installationOptions).find((opt) => opt.aliases && opt.aliases.includes(browserName));131132const handleClick = useCallback(133(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {134if (!browserOption) return;135136event.preventDefault();137138trackEvent("browser_extension_promotion_interaction", {139action: browserOption.type === "chromium" ? "chrome_navigation" : "firefox_navigation",140});141142window.open(browserOption.url, "_blank");143},144[browserOption],145);146147if (!isVisible || !browserName || !isFeatureFlagEnabled) {148return null;149}150151if (!scmProviderString || !usedProviders?.length) {152return null;153}154155if (!browserOption) {156return null;157}158159return (160<section className="flex justify-center mt-24 mx-4">161<div className="sm:flex justify-between border-pk-border-light border-2 rounded-xl hidden max-w-xl mt-4">162<div className="flex flex-col gap-1 py-5 pl-6 pr-8 justify-center">163<span className="text-lg font-semibold text-pk-content-secondary">164Open from {scmProviderString}165</span>166<span className="text-sm">167<a168href={browserOption.url}169target="_blank"170onClick={handleClick}171className="gp-link"172rel="noreferrer"173>174Install the Gitpod extension175</a>{" "}176to launch workspaces from {scmProviderString}.177</span>178</div>179<img180alt="A button that says Gitpod"181src={browserExtensionImages[usedProviders.at(0)!]}182className="w-32 h-fit self-end mb-4 mr-8"183/>184{/* <Button variant={"ghost"} onClick={handleClose} className="ml-3 self-start hover:bg-transparent">185<span className="sr-only">Close</span>186<XSvg className={cn("w-3 h-4 dark:text-white text-gray-700")} />187</Button> */}188</div>189</section>190);191}192193194