123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- import type {ReactNode} from 'react';
- import {Fragment, useCallback, useEffect, useState} from 'react';
- import styled from '@emotion/styled';
- import ButtonBar from 'sentry/components/buttonBar';
- import {Button, LinkButton} from 'sentry/components/core/button';
- import {SidebarPanelKey} from 'sentry/components/sidebar/types';
- import {t} from 'sentry/locale';
- import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
- import type {Organization} from 'sentry/types/organization';
- import useApi from 'sentry/utils/useApi';
- import useDismissAlert from 'sentry/utils/useDismissAlert';
- import {
- openAM2UpsellModal,
- openAM2UpsellModalSamePrice,
- } from 'getsentry/actionCreators/modal';
- import {sendReplayOnboardRequest} from 'getsentry/actionCreators/upsell';
- import usePreviewData from 'getsentry/components/upgradeNowModal/usePreviewData';
- import withSubscription from 'getsentry/components/withSubscription';
- import type {Subscription} from 'getsentry/types';
- import {PlanTier} from 'getsentry/types';
- import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
- import {redirectToManage} from './upgradeNowModal/utils';
- type ReplayOnboardingCTAUpsellProps = {
- organization: Organization;
- subscription: Subscription;
- };
- function ReplayOnboardingCTAUpsell({
- organization,
- subscription,
- }: ReplayOnboardingCTAUpsellProps) {
- const hasBillingAccess = organization.access?.includes('org:billing');
- const api = useApi();
- const {dismiss, isDismissed} = useDismissAlert({
- key: `${organization.id}:dismiss-replay-update-plan-button`,
- expirationDays: 14,
- });
- useEffect(() => {
- trackGetsentryAnalytics('replay.list_page.viewed', {
- organization,
- surface: 'replay_onboarding_banner',
- planTier: subscription.planTier,
- canSelfServe: subscription.canSelfServe,
- channel: subscription.channel,
- has_billing_scope: organization.access?.includes('org:billing'),
- });
- }, [organization, subscription]);
- const onEmailOwner = useCallback(async () => {
- await sendReplayOnboardRequest({
- orgSlug: organization.slug,
- api,
- currentPlan: 'am1-non-beta',
- onSuccess: () => {
- dismiss();
- trackGetsentryAnalytics('replay.list_page.sent_email', {
- organization,
- surface: 'replay_onboarding_banner',
- planTier: subscription.planTier,
- canSelfServe: subscription.canSelfServe,
- channel: subscription.channel,
- has_billing_scope: organization.access?.includes('org:billing'),
- });
- },
- });
- }, [api, organization, subscription, dismiss]);
- const [didClickOpenModal, setDidClickOpenModal] = useState<boolean>();
- const previewData = usePreviewData({
- organization,
- subscription,
- enabled: !subscription.canSelfServe || !hasBillingAccess,
- });
- const handleOpenModal = useCallback(() => {
- setDidClickOpenModal(true);
- }, []);
- // Once we have 1) previewData, and 2) the user clicked the button; then open the modal
- useEffect(() => {
- if (!didClickOpenModal || previewData.loading) {
- return;
- }
- if (previewData.error) {
- if (hasBillingAccess) {
- // Redirect the user to the subscriptions page, where they will find important information.
- // If they wish to update their plan, we ask them to contact our sales/support team.
- redirectToManage(organization);
- }
- return;
- }
- setDidClickOpenModal(false);
- const onComplete = () => {
- dismiss();
- trackGetsentryAnalytics('replay.list_page.open_modal', {
- organization,
- surface: 'replay_onboarding_banner',
- planTier: subscription.planTier,
- canSelfServe: subscription.canSelfServe,
- channel: subscription.channel,
- has_billing_scope: hasBillingAccess,
- has_price_change: previewData.previewData.billedAmount !== 0,
- });
- };
- if (hasBillingAccess && previewData.previewData.billedAmount === 0) {
- openAM2UpsellModalSamePrice({
- organization,
- subscription,
- onComplete: () => {
- window.location.hash = 'replay-sidequest';
- SidebarPanelStore.activatePanel(SidebarPanelKey.REPLAYS_ONBOARDING);
- onComplete();
- },
- surface: 'replay',
- ...previewData,
- });
- } else {
- openAM2UpsellModal({
- organization,
- subscription,
- isActionDisabled: isDismissed,
- onComplete,
- surface: 'replay',
- ...previewData,
- });
- }
- }, [
- dismiss,
- didClickOpenModal,
- hasBillingAccess,
- isDismissed,
- organization,
- previewData,
- subscription,
- ]);
- const onClickManageSubscription = useCallback(() => {
- trackGetsentryAnalytics('replay.list_page.manage_sub', {
- organization,
- surface: 'replay_onboarding_banner',
- planTier: subscription.planTier,
- canSelfServe: subscription.canSelfServe,
- channel: subscription.channel,
- has_billing_scope: organization.access?.includes('org:billing'),
- });
- }, [organization, subscription]);
- if (!subscription.canSelfServe) {
- // Two cases:
- // 1. Touch sales -> They need to call sales.
- // 2. Managed/Partner accounts, that are not AM2 -> no update path. They're stuck for now.
- // In either case the Subscription Overview page has a note about what options are available.
- return (
- <Fragment>
- <h3>{t('Get to the root cause faster')}</h3>
- {subscription.channel === 'sales' ? null : (
- <p>{t('Your current plan doesn’t support Session Replay.')}</p>
- )}
- <p>
- {t(
- 'Session Replay is a video-like reproduction of user interactions including page visits, mouse movements, clicks, and scrolls on a site or web app.'
- )}
- </p>
- <ButtonList gap={1}>
- <LinkButton
- to={`/settings/${organization.slug}/billing/overview/?referrer=replay_onboard-managed-cta`}
- onClick={onClickManageSubscription}
- priority="primary"
- >
- {t('Manage Subscription')}
- </LinkButton>
- <LinkButton href="https://docs.sentry.io/product/session-replay/" external>
- {t('Read Docs')}
- </LinkButton>
- </ButtonList>
- </Fragment>
- );
- }
- if ([PlanTier.MM1, PlanTier.MM2].includes(subscription.planTier as PlanTier)) {
- // MM1 & MM2 plans have no direct update path into AM2, prices could be wildly different
- // Members get an email, owners get to Manage Subscription
- return (
- <Fragment>
- <h3>{t('Get to the root cause faster')}</h3>
- <p>
- {t(
- 'Update to the latest version of your plan to get access to Session Replay and get video-like reproduction of user interactions including page visits, mouse movements, clicks, and scrolls on a site or web app.'
- )}
- </p>
- <ButtonList gap={1}>
- {hasBillingAccess ? (
- <LinkButton
- to={`/settings/${organization.slug}/billing/overview/?referrer=replay_onboard_mmx-cta`}
- onClick={onClickManageSubscription}
- priority="primary"
- >
- {t('Manage Subscription')}
- </LinkButton>
- ) : (
- <Button disabled={isDismissed} onClick={onEmailOwner} priority="primary">
- {t('Request to Update Plan')}
- </Button>
- )}
- <LinkButton href="https://docs.sentry.io/product/session-replay/" external>
- {t('Read Docs')}
- </LinkButton>
- </ButtonList>
- </Fragment>
- );
- }
- // AM1 orgs get a Modal which includes the one-click "Update Now" button
- return (
- <Fragment>
- <h3>{t('Get to the root cause faster')}</h3>
- <p>
- {t(
- 'Update to the latest version of your plan to get access to Session Replay and get video-like reproduction of user interactions including page visits, mouse movements, clicks, and scrolls on a site or web app.'
- )}
- </p>
- {hasBillingAccess ? null : (
- <p>{t('Notify your organization owner to start using Session Replay.')}</p>
- )}
- <ButtonList gap={1}>
- {hasBillingAccess ? (
- <Button
- onClick={handleOpenModal}
- priority="primary"
- disabled={didClickOpenModal && previewData.loading}
- >
- {t('Set Up Replays')}
- </Button>
- ) : (
- <Button disabled={isDismissed} onClick={onEmailOwner} priority="primary">
- {t('Notify Owner')}
- </Button>
- )}
- <LinkButton href="https://docs.sentry.io/product/session-replay/" external>
- {t('Read Docs')}
- </LinkButton>
- </ButtonList>
- </Fragment>
- );
- }
- const ButtonList = styled(ButtonBar)`
- grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
- `;
- type ReplayOnboardingCTAProps = {
- children: ReactNode;
- organization: Organization;
- subscription: Subscription;
- };
- /**
- * The majority of orgs have the replays feature, so we check for that first
- */
- function ReplayOnboardingCTA(props: ReplayOnboardingCTAProps) {
- const hasReplaysFeature = props.organization.features.includes('session-replay');
- if (hasReplaysFeature) {
- // AM2 orgs are ready to go, show the open source "Setup Replay SDK onboarding" panel
- // Also, any org that is trialing any AM2 plan is ready to go
- return <Fragment>{props.children}</Fragment>;
- }
- return <ReplayOnboardingCTAUpsell {...props} />;
- }
- export default withSubscription(ReplayOnboardingCTA, {noLoader: true});
|