diff --git a/airbyte-webapp/src/hooks/services/Experiment/experiments.ts b/airbyte-webapp/src/hooks/services/Experiment/experiments.ts index a06231626681..d7f4e8274f44 100644 --- a/airbyte-webapp/src/hooks/services/Experiment/experiments.ts +++ b/airbyte-webapp/src/hooks/services/Experiment/experiments.ts @@ -18,4 +18,5 @@ export interface Experiments { "authPage.oauth.github": boolean; "authPage.oauth.google.signUpPage": boolean; "authPage.oauth.github.signUpPage": boolean; + "onboarding.speedyConnection": boolean; } diff --git a/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx b/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx index d0daaefe5484..0d68f80e01ee 100644 --- a/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx +++ b/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx @@ -11,6 +11,7 @@ import { FeatureItem, FeatureSet, useFeatureService } from "hooks/services/Featu import { useApiHealthPoll } from "hooks/services/Health"; import { OnboardingServiceProvider } from "hooks/services/Onboarding"; import { useQuery } from "hooks/useQuery"; +import { useExperimentSpeedyConnection } from "packages/cloud/components/experiments/SpeedyConnection/hooks/useExperimentSpeedyConnection"; import { useAuthService } from "packages/cloud/services/auth/AuthService"; import { useIntercom } from "packages/cloud/services/thirdParty/intercom/useIntercom"; import { Auth } from "packages/cloud/views/auth"; @@ -86,6 +87,8 @@ const MainRoutes: React.FC = () => { const mainNavigate = workspace.displaySetupWizard && !hideOnboardingExperiment ? RoutePaths.Onboarding : RoutePaths.Connections; + // exp-speedy-connection + const { isExperimentVariant } = useExperimentSpeedyConnection(); return ( @@ -95,7 +98,7 @@ const MainRoutes: React.FC = () => { } /> } /> - {workspace.displaySetupWizard && !hideOnboardingExperiment && ( + {(workspace.displaySetupWizard || isExperimentVariant) && !hideOnboardingExperiment && ( = ({ expiredOfferDate }) => { + const [hours, minutes, seconds] = useCountdown(expiredOfferDate); + + return ( +
+ {hours.toString().padStart(2, "0")}h + {minutes.toString().padStart(2, "0")}m + {seconds.toString().padStart(2, "0")}s +
+ ); +}; diff --git a/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/CountDownTimer/index.ts b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/CountDownTimer/index.ts new file mode 100644 index 000000000000..002ded4737c3 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/CountDownTimer/index.ts @@ -0,0 +1 @@ +export * from "./CountDownTimer"; diff --git a/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/CountDownTimer/useCountdown.ts b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/CountDownTimer/useCountdown.ts new file mode 100644 index 000000000000..9c695a456c30 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/CountDownTimer/useCountdown.ts @@ -0,0 +1,26 @@ +import { useEffect, useState } from "react"; + +export const useCountdown = (targetDate: string) => { + const countDownDate = new Date(targetDate).getTime(); + + const [countDown, setCountDown] = useState(countDownDate - new Date().getTime()); + + useEffect(() => { + const interval = setInterval(() => { + setCountDown(countDownDate - new Date().getTime()); + }, 1000); + + return () => clearInterval(interval); + }, [countDownDate]); + + return getReturnValues(countDown); +}; + +const getReturnValues = (countDown: number): number[] => { + // calculate time left + const hours = Math.floor((countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((countDown % (1000 * 60)) / 1000); + + return [hours, minutes, seconds]; +}; diff --git a/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/SpeedyConnectionBanner.module.scss b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/SpeedyConnectionBanner.module.scss new file mode 100644 index 000000000000..2519746129c0 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/SpeedyConnectionBanner.module.scss @@ -0,0 +1,39 @@ +@use "../../../../../../../src/scss/variables"; +@use "../../../../../../../src/scss/colors"; + +.container { + height: 43px; + width: 100%; + position: fixed; + top: 0; + z-index: 3; + font-size: 12px; + line-height: 15px; + color: colors.$black; + padding: 8px; + display: flex; + align-items: center; + background-color: colors.$beige-100; + + @media (min-width: 1280px) { + height: 50px; + } +} + +.innerContainer { + display: flex; + width: 80%; + flex-direction: row; + gap: variables.$spacing-md; + justify-content: center; + align-items: center; + margin: auto; +} + +.linkCta { + color: colors.$dark-blue; +} + +.textDecorationNone { + text-decoration: none; +} diff --git a/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/SpeedyConnectionBanner.tsx b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/SpeedyConnectionBanner.tsx new file mode 100644 index 000000000000..01eca3ab1cb2 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/SpeedyConnectionBanner.tsx @@ -0,0 +1,51 @@ +import classnames from "classnames"; +import classNames from "classnames"; +import { FormattedMessage } from "react-intl"; +import { Link, useLocation } from "react-router-dom"; + +import { Text } from "components/ui/Text"; + +import { useExperiment } from "hooks/services/Experiment"; +import { CountDownTimer } from "packages/cloud/components/experiments/SpeedyConnection/CountDownTimer"; +import { StepType } from "pages/OnboardingPage/types"; +import { RoutePaths } from "pages/routePaths"; + +import { useExperimentSpeedyConnection } from "../hooks/useExperimentSpeedyConnection"; +import credits from "./credits.svg"; +import styles from "./SpeedyConnectionBanner.module.scss"; + +export const SpeedyConnectionBanner = () => { + const { expiredOfferDate } = useExperimentSpeedyConnection(); + const location = useLocation(); + const hideOnboardingExperiment = useExperiment("onboarding.hideOnboarding", false); + + return ( +
+
+ + + ( + + {link} + + ), + timer: () => , + }} + /> + +
+
+ ); +}; diff --git a/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/credits.svg b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/credits.svg new file mode 100644 index 000000000000..ec9380be9b1b --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/credits.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/index.ts b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/index.ts new file mode 100644 index 000000000000..b3709edb7b26 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner/index.ts @@ -0,0 +1 @@ +export * from "./SpeedyConnectionBanner"; diff --git a/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/hooks/useExperimentSpeedyConnection.ts b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/hooks/useExperimentSpeedyConnection.ts new file mode 100644 index 000000000000..c6727215bbd8 --- /dev/null +++ b/airbyte-webapp/src/packages/cloud/components/experiments/SpeedyConnection/hooks/useExperimentSpeedyConnection.ts @@ -0,0 +1,15 @@ +import { useExperiment } from "hooks/services/Experiment"; +import { useCurrentWorkspaceState } from "services/workspaces/WorkspacesService"; + +export const useExperimentSpeedyConnection = () => { + const { hasConnections } = useCurrentWorkspaceState(); + const isVariantEnabled = useExperiment("onboarding.speedyConnection", false); + + const timestamp = localStorage.getItem("exp-speedy-connection-timestamp"); + const expiredOfferDate = timestamp ? String(timestamp) : String(0); + + const now = new Date(); + const isExperimentVariant = + !hasConnections && expiredOfferDate && new Date(expiredOfferDate) >= now && isVariantEnabled; + return { isExperimentVariant, expiredOfferDate }; +}; diff --git a/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx b/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx index 539ac9f276a4..2cfac872ef42 100644 --- a/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx +++ b/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx @@ -127,6 +127,11 @@ export const AuthenticationProvider: React.FC> // also happen for email/password users if they closed their browser or got some network // errors in between creating the firebase user and the database user originally. const user = await createAirbyteUser(currentUser); + // exp-speedy-connection + localStorage.setItem( + "exp-speedy-connection-timestamp", + String(new Date(new Date().getTime() + 24 * 60 * 60 * 1000)) + ); await onAfterAuth(currentUser, user); } else { throw e; @@ -266,6 +271,11 @@ export const AuthenticationProvider: React.FC> await authService.sendEmailVerifiedLink(); if (auth.currentUser) { + // exp-speedy-connection + localStorage.setItem( + "exp-speedy-connection-timestamp", + String(new Date(new Date().getTime() + 24 * 60 * 60 * 1000)) + ); await onAfterAuth(auth.currentUser); } }, diff --git a/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.module.scss b/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.module.scss index f7b8ce4a4c4f..0d803d6f2d53 100644 --- a/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.module.scss +++ b/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.module.scss @@ -25,5 +25,13 @@ $banner-height: 30px; height: calc(100% - #{$banner-height}); } } + + &.speedyConnectionBanner { + margin-top: 50px; + + .dataBlock { + height: calc(100% - 50px); + } + } } } diff --git a/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.tsx b/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.tsx index 460fe334b61b..3b715057d52a 100644 --- a/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.tsx +++ b/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.tsx @@ -7,6 +7,8 @@ import { LoadingPage } from "components"; import { AlertBanner } from "components/ui/Banner/AlertBanner"; import { CloudRoutes } from "packages/cloud/cloudRoutes"; +import { useExperimentSpeedyConnection } from "packages/cloud/components/experiments/SpeedyConnection/hooks/useExperimentSpeedyConnection"; +import { SpeedyConnectionBanner } from "packages/cloud/components/experiments/SpeedyConnection/SpeedyConnectionBanner"; import { CreditStatus } from "packages/cloud/lib/domain/cloudWorkspaces/types"; import { useGetCloudWorkspace } from "packages/cloud/services/workspaces/CloudWorkspacesService"; import SideBar from "packages/cloud/views/layout/SideBar"; @@ -21,7 +23,6 @@ const MainView: React.FC> = (props) => { const { formatMessage } = useIntl(); const workspace = useCurrentWorkspace(); const cloudWorkspace = useGetCloudWorkspace(workspace.workspaceId); - const showCreditsBanner = cloudWorkspace.creditStatus && [ @@ -32,6 +33,10 @@ const MainView: React.FC> = (props) => { !cloudWorkspace.trialExpiryTimestamp; const alertToShow = showCreditsBanner ? "credits" : cloudWorkspace.trialExpiryTimestamp ? "trial" : undefined; + // exp-speedy-connection + const { isExperimentVariant } = useExperimentSpeedyConnection(); + const isTrial = Boolean(cloudWorkspace.trialExpiryTimestamp); + const showExperimentBanner = isExperimentVariant && isTrial; const alertMessage = useMemo(() => { if (alertToShow === "credits") { @@ -62,8 +67,13 @@ const MainView: React.FC> = (props) => {
}> -
- {alertToShow && } +
+ {showExperimentBanner ? : alertToShow && }
}> }>{props.children ?? } diff --git a/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx b/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx index 33010f626b0f..b45a2faab7c8 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx @@ -1,7 +1,6 @@ import React, { Suspense, useState } from "react"; import { FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; -import { useEffectOnce } from "react-use"; import styled from "styled-components"; import ApiErrorBoundary from "components/ApiErrorBoundary"; @@ -9,7 +8,7 @@ import HeadTitle from "components/HeadTitle"; import LoadingPage from "components/LoadingPage"; import { Button } from "components/ui/Button"; -import { useAnalyticsService, useTrackPage, PageTrackingCodes } from "hooks/services/Analytics"; +import { useTrackPage, PageTrackingCodes } from "hooks/services/Analytics"; import useWorkspace from "hooks/services/useWorkspace"; import { useCurrentWorkspaceState } from "services/workspaces/WorkspacesService"; import { ConnectorDocumentationWrapper } from "views/Connector/ConnectorDocumentationLayout"; @@ -60,15 +59,10 @@ const TITLE_BY_STEP: Partial> = { }; const OnboardingPage: React.FC = () => { - const analyticsService = useAnalyticsService(); useTrackPage(PageTrackingCodes.ONBOARDING); const navigate = useNavigate(); - useEffectOnce(() => { - analyticsService.page("Onboarding Page"); - }); - const { finishOnboarding } = useWorkspace(); const { hasConnections, hasDestinations, hasSources } = useCurrentWorkspaceState(); diff --git a/airbyte-webapp/src/pages/OnboardingPage/components/WelcomeStep.tsx b/airbyte-webapp/src/pages/OnboardingPage/components/WelcomeStep.tsx index ea1c86b820b7..0a5c338124ce 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/components/WelcomeStep.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/components/WelcomeStep.tsx @@ -66,6 +66,7 @@ const WelcomeStep: React.FC = ({ userName, onNextStep }) => { link={links.demoLink} /> + diff --git a/airbyte-webapp/src/pages/OnboardingPage/types.ts b/airbyte-webapp/src/pages/OnboardingPage/types.ts index 37974abbe879..f6acb1aaa800 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/types.ts +++ b/airbyte-webapp/src/pages/OnboardingPage/types.ts @@ -5,3 +5,8 @@ export enum StepType { SET_UP_CONNECTION = "set-up-connection", FINAL = "final", } + +// exp-speedy-connection +export interface ILocationState extends Omit { + state: Type; +} diff --git a/airbyte-webapp/src/pages/OnboardingPage/useStepsConfig.tsx b/airbyte-webapp/src/pages/OnboardingPage/useStepsConfig.tsx index c95d5cb84b7b..71cb7b26098d 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/useStepsConfig.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/useStepsConfig.tsx @@ -1,7 +1,8 @@ import { useMemo, useState, useCallback } from "react"; import { FormattedMessage } from "react-intl"; +import { useLocation } from "react-router-dom"; -import { StepType } from "./types"; +import { ILocationState, StepType } from "./types"; const useStepsConfig = ( hasSources: boolean, @@ -13,7 +14,14 @@ const useStepsConfig = ( setCurrentStep: (step: StepType) => void; steps: Array<{ name: JSX.Element; id: StepType }>; } => { + // exp-speedy-connection + const location = useLocation() as unknown as ILocationState<{ step: StepType }>; + const getInitialStep = () => { + // exp-speedy-connection + if (location.state?.step) { + return location.state.step; + } if (hasSources) { if (hasDestinations) { if (hasConnections) {