diff --git a/client/web/BUILD.bazel b/client/web/BUILD.bazel index d0262208d868..5bd898d51828 100644 --- a/client/web/BUILD.bazel +++ b/client/web/BUILD.bazel @@ -281,6 +281,7 @@ ts_project( "src/cody/sidebar/useSidebarSize.tsx", "src/cody/subscription/CodySubscriptionPage.tsx", "src/cody/subscription/queries.tsx", + "src/cody/subscription/useUserCodySubscription.ts", "src/cody/switch-account/CodySwitchAccountPage.tsx", "src/cody/team/CodyManageTeamPage.tsx", "src/cody/team/TeamMemberList.tsx", diff --git a/client/web/src/cody/codyProRoutes.tsx b/client/web/src/cody/codyProRoutes.tsx index e390e2ead64e..7889635c4ed4 100644 --- a/client/web/src/cody/codyProRoutes.tsx +++ b/client/web/src/cody/codyProRoutes.tsx @@ -1,10 +1,12 @@ -import type { RouteObject } from 'react-router-dom' +import { Navigate, type RouteObject } from 'react-router-dom' +import { logger } from '@sourcegraph/common' import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent' import { type LegacyLayoutRouteContext, LegacyRoute } from '../LegacyRouteContext' import { QueryClientProvider } from './management/api/react-query/QueryClientProvider' +import { useUserCodySubscription } from './subscription/useUserCodySubscription' import { isEmbeddedCodyProUIEnabled } from './util' export enum CodyProRoutes { @@ -77,10 +79,29 @@ interface CodyProPageProps extends Pick = props => { + const { data } = useUserCodySubscription() + if (!data) { + return null + } + + if (!data.currentUser) { + // Cody plan is not available if the user is not authenticated. Redirecting to the sign-in page. + return + } + + if (!data.currentUser.codySubscription) { + logger.error('Cody subscription data is not available.') + return null + } + const Component = routeComponents[props.path] return ( - + ) } diff --git a/client/web/src/cody/invites/AcceptInviteBanner.tsx b/client/web/src/cody/invites/AcceptInviteBanner.tsx index 0af1cda0cdc3..57d8ee4601c3 100644 --- a/client/web/src/cody/invites/AcceptInviteBanner.tsx +++ b/client/web/src/cody/invites/AcceptInviteBanner.tsx @@ -3,11 +3,12 @@ import { Button, ButtonLink, H1, Text } from '@sourcegraph/wildcard' import { CodyProRoutes } from '../codyProRoutes' import { CodyAlert } from '../components/CodyAlert' import { useAcceptInvite, useCancelInvite } from '../management/api/react-query/invites' +import { useUserCodySubscription } from '../subscription/useUserCodySubscription' import { useInviteParams } from './useInviteParams' import { UserInviteStatus, useInviteState } from './useInviteState' -export const AcceptInviteBanner: React.FC<{ onSuccess: () => unknown }> = ({ onSuccess }) => { +export const AcceptInviteBanner: React.FC = () => { const { inviteParams, clearInviteParams } = useInviteParams() if (!inviteParams) { return null @@ -16,18 +17,17 @@ export const AcceptInviteBanner: React.FC<{ onSuccess: () => unknown }> = ({ onS ) } -const AcceptInviteBannerContent: React.FC<{ - teamId: string - inviteId: string - onSuccess: () => unknown - clearInviteParams: () => void -}> = ({ teamId, inviteId, onSuccess, clearInviteParams }) => { +const AcceptInviteBannerContent: React.FC<{ teamId: string; inviteId: string; clearInviteParams: () => void }> = ({ + teamId, + inviteId, + clearInviteParams, +}) => { + const { refetch } = useUserCodySubscription() const inviteState = useInviteState(teamId, inviteId) const acceptInviteMutation = useAcceptInvite() const cancelInviteMutation = useCancelInvite() @@ -108,7 +108,7 @@ const AcceptInviteBannerContent: React.FC<{ onClick={() => acceptInviteMutation.mutate( { teamId, inviteId }, - { onSuccess, onSettled: clearInviteParams } + { onSuccess: () => refetch(), onSettled: clearInviteParams } ) } > diff --git a/client/web/src/cody/management/CodyManagementPage.tsx b/client/web/src/cody/management/CodyManagementPage.tsx index 6db526e69b24..a044f5269613 100644 --- a/client/web/src/cody/management/CodyManagementPage.tsx +++ b/client/web/src/cody/management/CodyManagementPage.tsx @@ -11,13 +11,7 @@ import { ButtonLink, H1, H2, Icon, Link, PageHeader, Text, useSearchParameters, import type { AuthenticatedUser } from '../../auth' import { Page } from '../../components/Page' import { PageTitle } from '../../components/PageTitle' -import { - type UserCodyPlanResult, - type UserCodyPlanVariables, - type UserCodyUsageResult, - type UserCodyUsageVariables, - CodySubscriptionPlan, -} from '../../graphql-operations' +import { type UserCodyUsageResult, type UserCodyUsageVariables, CodySubscriptionPlan } from '../../graphql-operations' import { CodyProRoutes } from '../codyProRoutes' import { CodyAlert } from '../components/CodyAlert' import { ProIcon } from '../components/CodyIcon' @@ -26,7 +20,8 @@ import { AcceptInviteBanner } from '../invites/AcceptInviteBanner' import { InviteUsers } from '../invites/InviteUsers' import { isCodyEnabled } from '../isCodyEnabled' import { CodyOnboarding, type IEditor } from '../onboarding/CodyOnboarding' -import { USER_CODY_PLAN, USER_CODY_USAGE } from '../subscription/queries' +import { USER_CODY_USAGE } from '../subscription/queries' +import type { UserCodySubscription } from '../subscription/useUserCodySubscription' import { getManageSubscriptionPageURL } from '../util' import { useSubscriptionSummary } from './api/react-query/subscriptions' @@ -37,6 +32,7 @@ import styles from './CodyManagementPage.module.scss' interface CodyManagementPageProps extends TelemetryV2Props { authenticatedUser: AuthenticatedUser | null + codySubscription: UserCodySubscription } export enum EditorStep { @@ -47,6 +43,7 @@ export enum EditorStep { export const CodyManagementPage: React.FunctionComponent = ({ authenticatedUser, telemetryRecorder, + codySubscription, }) => { const navigate = useNavigate() const parameters = useSearchParameters() @@ -69,8 +66,6 @@ export const CodyManagementPage: React.FunctionComponent(USER_CODY_PLAN, {}) - const { data: usageData, error: usageDateError } = useQuery( USER_CODY_USAGE, {} @@ -82,14 +77,6 @@ export const CodyManagementPage: React.FunctionComponent(null) const [selectedEditorStep, setSelectedEditorStep] = React.useState(null) - const subscription = data?.currentUser?.codySubscription - - useEffect(() => { - if (!!data && !data?.currentUser) { - navigate(`/sign-in?returnTo=${CodyProRoutes.Manage}`) - } - }, [data, navigate]) - const getTeamInviteButton = (): JSX.Element | null => { const isSoloUser = subscriptionSummaryQueryResult?.data?.teamMaxMembers === 1 const hasFreeSeats = subscriptionSummaryQueryResult?.data @@ -114,25 +101,21 @@ export const CodyManagementPage: React.FunctionComponent - + {welcomeToPro && (

Welcome to Cody Pro

@@ -191,7 +174,7 @@ export const CodyManagementPage: React.FunctionComponent )} - + { authenticatedUser: AuthenticatedUser + codySubscription: UserCodySubscription } -const AuthenticatedCodySubscriptionManagePage: React.FC = ({ telemetryRecorder }) => { - const { - loading: userCodyPlanLoading, - error: useCodyPlanError, - data: userCodyPlanData, - } = useQuery(USER_CODY_PLAN, {}) +const AuthenticatedCodySubscriptionManagePage: React.FC = ({ telemetryRecorder, codySubscription }) => { + const subscriptionSummaryQuery = useSubscriptionSummary() useEffect( function recordViewEvent() { @@ -46,24 +38,23 @@ const AuthenticatedCodySubscriptionManagePage: React.FC = ({ telemetryRec [telemetryRecorder] ) - if (userCodyPlanLoading) { + if (subscriptionSummaryQuery.isLoading) { return } - if (useCodyPlanError) { - logger.error('Failed to fetch Cody subscription data', useCodyPlanError) + if (subscriptionSummaryQuery.isError) { + logger.error('Failed to fetch Cody subscription summary', subscriptionSummaryQuery.error) return null } - const subscriptionData = userCodyPlanData?.currentUser?.codySubscription - if (!subscriptionData) { - logger.error('Cody subscription data is not available.') + if (!subscriptionSummaryQuery.data) { + logger.error('Cody subscription summary is not available.') return null } // This page only applies to users who have a Cody Pro subscription to manage. // Otherwise, direct them to the ./new page to sign up. - if (subscriptionData.plan !== CodySubscriptionPlan.PRO) { + if (codySubscription.plan !== CodySubscriptionPlan.PRO) { return } diff --git a/client/web/src/cody/management/subscription/new/NewCodyProSubscriptionPage.tsx b/client/web/src/cody/management/subscription/new/NewCodyProSubscriptionPage.tsx index d922b958c943..e2e003e3d730 100644 --- a/client/web/src/cody/management/subscription/new/NewCodyProSubscriptionPage.tsx +++ b/client/web/src/cody/management/subscription/new/NewCodyProSubscriptionPage.tsx @@ -9,7 +9,6 @@ import { loadStripe } from '@stripe/stripe-js' import classNames from 'classnames' import { Navigate, useSearchParams } from 'react-router-dom' -import { useQuery } from '@sourcegraph/http-client' import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import { PageHeader, Text, LoadingSpinner, Alert, logger } from '@sourcegraph/wildcard' @@ -17,14 +16,10 @@ import type { AuthenticatedUser } from '../../../../auth' import { withAuthenticatedUser } from '../../../../auth/withAuthenticatedUser' import { Page } from '../../../../components/Page' import { PageTitle } from '../../../../components/PageTitle' -import { - type UserCodyPlanResult, - type UserCodyPlanVariables, - CodySubscriptionPlan, -} from '../../../../graphql-operations' +import { CodySubscriptionPlan } from '../../../../graphql-operations' import { CodyProRoutes } from '../../../codyProRoutes' import { PageHeaderIcon } from '../../../components/PageHeaderIcon' -import { USER_CODY_PLAN } from '../../../subscription/queries' +import type { UserCodySubscription } from '../../../subscription/useUserCodySubscription' import { defaultCodyProApiClientContext, CodyProApiClientContext } from '../../api/components/CodyProApiClient' import { useCurrentSubscription } from '../../api/react-query/subscriptions' import { useBillingAddressStripeElementsOptions } from '../manage/BillingAddress' @@ -41,11 +36,13 @@ const stripe = await loadStripe(publishableKey || '') interface NewCodyProSubscriptionPageProps extends TelemetryV2Props { authenticatedUser: AuthenticatedUser + codySubscription: UserCodySubscription } const AuthenticatedNewCodyProSubscriptionPage: FunctionComponent = ({ authenticatedUser, telemetryRecorder, + codySubscription, }) => { const [urlSearchParams] = useSearchParams() const addSeats = !!urlSearchParams.get('addSeats') @@ -53,12 +50,6 @@ const AuthenticatedNewCodyProSubscriptionPage: FunctionComponent(USER_CODY_PLAN, {}) const subscriptionQueryResult = useCurrentSubscription() const subscription = subscriptionQueryResult?.data @@ -67,25 +58,21 @@ const AuthenticatedNewCodyProSubscriptionPage: FunctionComponent { - if (userCodyPlanError) { - logger.error('Failed to fetch subscription data', userCodyPlanError) - } - }, [userCodyPlanError]) - useEffect(() => { - if (subscriptionQueryResult.isError) { + if (subscriptionQueryResult.error) { logger.error('Failed to fetch subscription data', subscriptionQueryResult.error) } - }, [subscriptionQueryResult.isError, subscriptionQueryResult.error]) - - if (userCodyPlanLoading || subscriptionQueryResult.isLoading) { - return - } + }, [subscriptionQueryResult.error]) // If the user already has a Cody Pro subscription, direct them back to the Cody Management page. - if (!addSeats && userCodyPlanData?.currentUser?.codySubscription?.plan === CodySubscriptionPlan.PRO) { + if (!addSeats && codySubscription.plan === CodySubscriptionPlan.PRO) { return } + // Display spinner without page header because without the subscription, we don't know which header to show + if (subscriptionQueryResult.isLoading) { + return + } + const PageWithHeader = ({ children }: { children: React.ReactNode }): React.ReactElement => ( @@ -103,7 +90,7 @@ const AuthenticatedNewCodyProSubscriptionPage: FunctionComponent ) - if (userCodyPlanLoading || subscriptionQueryResult.isLoading) { + if (subscriptionQueryResult.isLoading) { return ( @@ -111,14 +98,6 @@ const AuthenticatedNewCodyProSubscriptionPage: FunctionComponent - Failed to fetch user Cody plan data - - ) - } - if (addSeats && subscriptionQueryResult.isError) { return ( diff --git a/client/web/src/cody/subscription/CodySubscriptionPage.tsx b/client/web/src/cody/subscription/CodySubscriptionPage.tsx index 2a174cf8a8cf..b9eb7ea0b346 100644 --- a/client/web/src/cody/subscription/CodySubscriptionPage.tsx +++ b/client/web/src/cody/subscription/CodySubscriptionPage.tsx @@ -4,7 +4,6 @@ import { mdiArrowLeft, mdiInformationOutline, mdiTrendingUp, mdiCreditCardOutlin import classNames from 'classnames' import { useNavigate } from 'react-router-dom' -import { useQuery } from '@sourcegraph/http-client' import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' import { Badge, @@ -21,27 +20,27 @@ import { } from '@sourcegraph/wildcard' import type { AuthenticatedUser } from '../../auth' +import { withAuthenticatedUser } from '../../auth/withAuthenticatedUser' import { Page } from '../../components/Page' import { PageTitle } from '../../components/PageTitle' import { CodySubscriptionPlan } from '../../graphql-operations' -import type { UserCodyPlanResult, UserCodyPlanVariables } from '../../graphql-operations' import { CodyProRoutes } from '../codyProRoutes' import { ProIcon } from '../components/CodyIcon' import { PageHeaderIcon } from '../components/PageHeaderIcon' -import { isCodyEnabled } from '../isCodyEnabled' import { getManageSubscriptionPageURL, isEmbeddedCodyProUIEnabled, manageSubscriptionRedirectURL } from '../util' -import { USER_CODY_PLAN } from './queries' +import type { UserCodySubscription } from './useUserCodySubscription' import styles from './CodySubscriptionPage.module.scss' interface CodySubscriptionPageProps extends TelemetryV2Props { - authenticatedUser?: AuthenticatedUser | null + authenticatedUser: AuthenticatedUser + codySubscription: UserCodySubscription } -export const CodySubscriptionPage: React.FunctionComponent = ({ - authenticatedUser, +const AuthenticatedCodySubscriptionPage: React.FunctionComponent = ({ telemetryRecorder, + codySubscription, }) => { const parameters = useSearchParameters() @@ -50,26 +49,10 @@ export const CodySubscriptionPage: React.FunctionComponent(USER_CODY_PLAN, {}) - const navigate = useNavigate() const useEmbeddedCodyUI = useMemo(() => isEmbeddedCodyProUIEnabled(), []) - useEffect(() => { - if (!!data && !data?.currentUser) { - navigate(`/sign-in?returnTo=${CodyProRoutes.Subscription}`) - } - }, [data, navigate]) - - if (dataError) { - throw dataError - } - - if (!isCodyEnabled() || !data?.currentUser || !authenticatedUser) { - return null - } - - const isProUser = data.currentUser.codySubscription?.plan === CodySubscriptionPlan.PRO + const isProUser = codySubscription.plan === CodySubscriptionPlan.PRO return ( <> @@ -454,6 +437,8 @@ export const CodySubscriptionPage: React.FunctionComponent ( >['codySubscription']>, + '__typename' +> + +const USER_CODY_PLAN = gql` + query UserCodyPlan { + currentUser { + id + codySubscription { + status + plan + applyProRateLimits + currentPeriodStartAt + currentPeriodEndAt + cancelAtPeriodEnd + } + } + } +` + +export const useUserCodySubscription = (): QueryResult => + useQuery(USER_CODY_PLAN, {}) diff --git a/client/web/src/cody/team/CodyManageTeamPage.tsx b/client/web/src/cody/team/CodyManageTeamPage.tsx index 4fea4ee10369..0c01b141db75 100644 --- a/client/web/src/cody/team/CodyManageTeamPage.tsx +++ b/client/web/src/cody/team/CodyManageTeamPage.tsx @@ -18,11 +18,13 @@ import { InviteUsers } from '../invites/InviteUsers' import { useTeamInvites } from '../management/api/react-query/invites' import { useCurrentSubscription, useSubscriptionSummary } from '../management/api/react-query/subscriptions' import { useTeamMembers } from '../management/api/react-query/teams' +import type { UserCodySubscription } from '../subscription/useUserCodySubscription' import { TeamMemberList } from './TeamMemberList' interface CodyManageTeamPageProps extends TelemetryV2Props { authenticatedUser: AuthenticatedUser + codySubscription: UserCodySubscription } const AuthenticatedCodyManageTeamPage: React.FunctionComponent = ({ telemetryRecorder }) => {