Skip to content

Commit

Permalink
Browser Extension Advertisement (gitpod-io#19650)
Browse files Browse the repository at this point in the history
* Browser Extension in-app banner

* add icons

* Prevent undefined browsers

* Local storage control

* use sessionstorage

* Tweak colors and hide on small viewports

* Dismiss button

* Universal icon

* fix icon

* reset button

* Remove unused class

* Fix close button?

* Modify layout

* add missing browsers

* Fix extra whitespace

* do not show when autostarting

* remove duplicate type dep

* Prevent flashing

* Apply new copy

Co-authored-by: Lou Bichard <[email protected]>

* Address review comments

Co-authored-by: Brad Harris <[email protected]>

* Fix svg path

* fix colors

* Layout

* minor changis

* 🤷‍♂️

* Fix spacing and max width

* Layout fixes

* Bigger top margin

* Dynamic auth provider message

* Update dashboard typescript

* ui updates

* Fix center alignment

I literally have no idea why this works

* sort provider names and show only when a ff is on

* Track events

* Open in new tab

* margin to the right of the img

* Rename event

* Remove duplicate sort

* Organize imports

* some design tweaks from pr review

* extend right padding

* fix text size

* revert text size change and update screenshot padding

---------

Co-authored-by: Lou Bichard <[email protected]>
Co-authored-by: Brad Harris <[email protected]>
  • Loading branch information
3 people authored Apr 26, 2024
1 parent f48dc13 commit 8f3888d
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 1 deletion.
4 changes: 3 additions & 1 deletion components/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@tanstack/react-query-devtools": "^4.29.19",
"@tanstack/react-query-persist-client": "^4.29.19",
"@types/react-datepicker": "^4.8.0",
"@types/ua-parser-js": "^0.7.37",
"buffer": "^4.3.0",
"class-variance-authority": "^0.7.0",
"classnames": "^2.3.1",
Expand Down Expand Up @@ -56,6 +57,7 @@
"stream-browserify": "^2.0.1",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"ua-parser-js": "^1.0.36",
"url": "^0.11.1",
"use-deep-compare-effect": "^1.8.1",
"util": "^0.11.1",
Expand Down Expand Up @@ -94,7 +96,7 @@
"tailwind-underline-utils": "^1.1.2",
"tailwindcss": "^3.4.3",
"tailwindcss-filters": "^3.0.0",
"typescript": "~4.4.2",
"typescript": "^5.4.5",
"web-vitals": "^1.1.1"
},
"scripts": {
Expand Down
10 changes: 10 additions & 0 deletions components/dashboard/src/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type Event =
| "feedback_submitted"
| "workspace_class_changed"
| "privacy_policy_update_accepted"
| "browser_extension_promotion_interaction"
| "coachmark_dismissed"
| "modal_dismiss"
| "ide_configuration_changed"
Expand All @@ -30,6 +31,7 @@ export type EventProperties =
| TrackDotfileRepo
| TrackFeedback
| TrackPolicyUpdateClick
| TrackBrowserExtensionPromotionInteraction
| TrackModalDismiss
| TrackIDEConfigurationChanged
| TrackWorkspaceClassChanged
Expand Down Expand Up @@ -99,6 +101,10 @@ export interface TrackCoachmarkDismissed {
success: boolean;
}

export interface TrackBrowserExtensionPromotionInteraction {
action: "chrome_navigation" | "firefox_navigation" | "manually_dismissed";
}

interface TrackDashboardClick {
dnt?: boolean;
path: string;
Expand Down Expand Up @@ -126,6 +132,10 @@ export function trackEvent(event: "feedback_submitted", properties: TrackFeedbac
export function trackEvent(event: "workspace_class_changed", properties: TrackWorkspaceClassChanged): void;
export function trackEvent(event: "privacy_policy_update_accepted", properties: TrackPolicyUpdateClick): void;
export function trackEvent(event: "coachmark_dismissed", properties: TrackCoachmarkDismissed): void;
export function trackEvent(
event: "browser_extension_promotion_interaction",
properties: TrackBrowserExtensionPromotionInteraction,
): void;
export function trackEvent(event: "modal_dismiss", properties: TrackModalDismiss): void;
export function trackEvent(event: "ide_configuration_changed", properties: TrackIDEConfigurationChanged): void;
export function trackEvent(event: "status_rendered", properties: TrackStatusRendered): void;
Expand Down
1 change: 1 addition & 0 deletions components/dashboard/src/data/featureflag-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const featureFlags = {
dataops: false,
// Logging tracing for added for investigate hanging issue
dashboard_logging_tracing: false,
showBrowserExtensionPromotion: false,
};

type FeatureFlags = typeof featureFlags;
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions components/dashboard/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@

/* Borders */
--border-base: var(--gray-200);
--border-light: var(--gray-100);
}

/* Dark mode color adjustments */
Expand All @@ -73,6 +74,7 @@

/* Borders */
--border-base: var(--gray-700);
--border-light: var(--gray-800);
}

html,
Expand Down
185 changes: 185 additions & 0 deletions components/dashboard/src/workspaces/BrowserExtensionBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* 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, useMemo, useState } from "react";
import UAParser from "ua-parser-js";
import { useUserLoader } from "../hooks/use-user-loader";
import { User } from "@gitpod/public-api/lib/gitpod/v1/user_pb";
import { AuthProviderDescription, AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
import { useAuthProviderDescriptions } from "../data/auth-providers/auth-provider-descriptions-query";
import { useFeatureFlag } from "../data/featureflag-query";
import { trackEvent } from "../Analytics";

import bitbucketButton from "../images/browser-extension/bitbucket.webp";
import githubButton from "../images/browser-extension/github.webp";
import gitlabButton from "../images/browser-extension/gitlab.webp";

const browserExtensionImages = {
Bitbucket: bitbucketButton,
GitHub: githubButton,
GitLab: gitlabButton,
} as const;

type BrowserOption = {
type: "firefox" | "chromium";
aliases?: string[];
url: string;
};
type UnifiedAuthProvider = "Bitbucket" | "GitLab" | "GitHub";

const installationOptions: BrowserOption[] = [
{
type: "firefox",
aliases: ["firefox"],
url: "https://addons.mozilla.org/en-US/firefox/addon/gitpod/",
},
{
type: "chromium",
aliases: ["chrome", "edge", "brave", "chromium", "vivaldi", "opera"],
url: "https://chrome.google.com/webstore/detail/gitpod-always-ready-to-co/dodmmooeoklaejobgleioelladacbeki",
},
];

const isIdentity = (identity?: AuthProviderDescription): identity is AuthProviderDescription => !!identity;
const unifyProviderType = (type: AuthProviderType): UnifiedAuthProvider | undefined => {
switch (type) {
case AuthProviderType.BITBUCKET:
case AuthProviderType.BITBUCKET_SERVER:
return "Bitbucket";
case AuthProviderType.GITHUB:
return "GitHub";
case AuthProviderType.GITLAB:
return "GitLab";
default:
return undefined;
}
};

const isAuthProviderType = (type?: UnifiedAuthProvider): type is UnifiedAuthProvider => !!type;
const getDeduplicatedScmProviders = (user: User, descriptions: AuthProviderDescription[]): UnifiedAuthProvider[] => {
const userIdentities = user.identities.map((identity) => identity.authProviderId);
const userProviders = userIdentities
.map((id) => descriptions?.find((provider) => provider.id === id))
.filter(isIdentity)
.map((provider) => provider.type);

return userProviders
.map((type) => unifyProviderType(type))
.filter(isAuthProviderType)
.sort();
};

const displayScmProviders = (providers: UnifiedAuthProvider[]): string => {
const formatter = new Intl.ListFormat("en", { style: "long", type: "disjunction" });

return formatter.format(providers);
};

export function BrowserExtensionBanner() {
const { user } = useUserLoader();
const { data: authProviderDescriptions } = useAuthProviderDescriptions();

const usedProviders = useMemo(() => {
if (!user || !authProviderDescriptions) return;

return getDeduplicatedScmProviders(user, authProviderDescriptions);
}, [user, authProviderDescriptions]);

const scmProviderString = useMemo(() => usedProviders && displayScmProviders(usedProviders), [usedProviders]);

const parser = useMemo(() => new UAParser(), []);
const browserName = useMemo(() => parser.getBrowser().name?.toLowerCase(), [parser]);

const [isVisible, setIsVisible] = useState(false);
const isFeatureFlagEnabled = useFeatureFlag("showBrowserExtensionPromotion");

useEffect(() => {
const installedOrDismissed =
sessionStorage.getItem("browser-extension-installed") ||
localStorage.getItem("browser-extension-banner-dismissed");

setIsVisible(!installedOrDismissed);
}, []);

// const handleClose = () => {
// let persistSuccess = true;
// try {
// localStorage.setItem("browser-extension-banner-dismissed", "true");
// } catch (e) {
// persistSuccess = false;
// } finally {
// setIsVisible(false);
// trackEvent("coachmark_dismissed", {
// name: "browser_extension_promotion",
// success: persistSuccess,
// });
// }
// };

const browserOption =
browserName &&
Object.values(installationOptions).find((opt) => opt.aliases && opt.aliases.includes(browserName));

const handleClick = useCallback(
(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (!browserOption) return;

event.preventDefault();

trackEvent("browser_extension_promotion_interaction", {
action: browserOption.type === "chromium" ? "chrome_navigation" : "firefox_navigation",
});

window.open(browserOption.url, "_blank");
},
[browserOption],
);

if (!isVisible || !browserName || !isFeatureFlagEnabled) {
return null;
}

if (!scmProviderString || !usedProviders?.length) {
return null;
}

if (!browserOption) {
return null;
}

return (
<section className="flex justify-center mt-24 mx-4">
<div className="sm:flex justify-between border-pk-border-light border-2 rounded-xl hidden max-w-xl mt-4">
<div className="flex flex-col gap-1 py-5 pl-6 pr-8 justify-center">
<span className="text-lg font-semibold text-pk-content-secondary">
Open from {scmProviderString}
</span>
<span className="text-sm">
<a
href={browserOption.url}
target="_blank"
onClick={handleClick}
className="gp-link"
rel="noreferrer"
>
Install the Gitpod extension
</a>{" "}
to launch workspaces from {scmProviderString}.
</span>
</div>
<img
alt="A button that says Gitpod"
src={browserExtensionImages[usedProviders.at(0)!]}
className="w-32 h-fit self-end mb-4 mr-8"
/>
{/* <Button variant={"ghost"} onClick={handleClose} className="ml-3 self-start hover:bg-transparent">
<span className="sr-only">Close</span>
<XSvg className={cn("w-3 h-4 dark:text-white text-gray-700")} />
</Button> */}
</div>
</section>
);
}
2 changes: 2 additions & 0 deletions components/dashboard/src/workspaces/CreateWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { StartWorkspaceOptions } from "../start/start-workspace-options";
import { UserContext, useCurrentUser } from "../user-context";
import { SelectAccountModal } from "../user-settings/SelectAccountModal";
import { settingsPathIntegrations } from "../user-settings/settings.routes";
import { BrowserExtensionBanner } from "./BrowserExtensionBanner";
import { WorkspaceEntry } from "./WorkspaceEntry";
import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
import {
Expand Down Expand Up @@ -565,6 +566,7 @@ export function CreateWorkspacePage() {
)}
</div>
</div>
{!autostart && <BrowserExtensionBanner />}
</div>
);
}
Expand Down
1 change: 1 addition & 0 deletions components/dashboard/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ module.exports = {
},
"pk-border": {
base: "rgb(var(--border-base) / <alpha-value>)",
light: "rgb(var(--border-light) / <alpha-value>)"
},
},
backgroundImage: {
Expand Down
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4076,6 +4076,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==

"@types/ua-parser-js@^0.7.37":
version "0.7.37"
resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.37.tgz#2e45bf948a6a94391859a1b0682104ae3c13ba72"
integrity sha512-4sOxS3ZWXC0uHJLYcWAaLMxTvjRX3hT96eF4YWUh1ovTaenvibaZOE5uXtIp4mksKMLRwo7YDiCBCw6vBiUPVg==

"@types/[email protected]", "@types/uuid@^8.3.1":
version "8.3.1"
resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz"
Expand Down Expand Up @@ -15068,6 +15073,16 @@ typescript@^4.2.4, typescript@~4.4.2, typescript@~4.4.4:
resolved "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz"
integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==

typescript@^5.4.5:
version "5.4.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==

ua-parser-js@^1.0.36:
version "1.0.36"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.36.tgz#a9ab6b9bd3a8efb90bb0816674b412717b7c428c"
integrity sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==

uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
Expand Down

0 comments on commit 8f3888d

Please sign in to comment.