Browse Source

feat(onboarding): mobile onboarding redirect view (#34129)

* feat(onboarding): Mobile onboarding redirect

* fix

* changes
Zhixing Zhang 2 years ago
parent
commit
6307d9aad5

+ 6 - 0
static/app/data/experimentConfig.tsx

@@ -19,6 +19,12 @@ export const experimentList = [
     parameter: 'exposed',
     assignments: [0, 1],
   },
+  {
+    key: 'TargetedOnboardingMobileRedirectExperiment',
+    type: ExperimentType.Organization,
+    parameter: 'scenario',
+    assignments: ['none', 'email-cta', 'hide'],
+  },
 ] as const;
 
 export const experimentConfig = experimentList.reduce(

+ 10 - 0
static/app/utils/isMobile.ts

@@ -0,0 +1,10 @@
+/**
+ * Checks if the user agent is a mobile device. On browsers that does not support `navigator.userAgentData`,
+ * fallback to checking the viewport width.
+ */
+export default function isMobile(): boolean {
+  if ((navigator as any).userAgentData) {
+    return (navigator as any).userAgentData.mobile;
+  }
+  return window.innerWidth < 800;
+}

+ 24 - 1
static/app/views/onboarding/targetedOnboarding/components/createProjectsFooter.tsx

@@ -1,4 +1,5 @@
 import {Fragment} from 'react';
+import {browserHistory} from 'react-router';
 import styled from '@emotion/styled';
 import * as Sentry from '@sentry/react';
 import {motion} from 'framer-motion';
@@ -17,6 +18,7 @@ import {t, tn} from 'sentry/locale';
 import space from 'sentry/styles/space';
 import {Organization} from 'sentry/types';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
+import isMobile from 'sentry/utils/isMobile';
 import testableTransition from 'sentry/utils/testableTransition';
 import useApi from 'sentry/utils/useApi';
 import useTeams from 'sentry/utils/useTeams';
@@ -63,11 +65,13 @@ export default function CreateProjectsFooter({
             })
           )
       );
+      const shouldSendEmail = !persistedOnboardingState.mobileEmailSent;
       const nextState: OnboardingState = {
         platformToProjectIdMap: persistedOnboardingState.platformToProjectIdMap,
         selectedPlatforms: platforms,
         state: 'projects_selected',
         url: 'setup-docs/',
+        mobileEmailSent: true,
       };
       responses.forEach(p => (nextState.platformToProjectIdMap[p.platform] = p.slug));
       setPersistedOnboardingState(nextState);
@@ -79,7 +83,26 @@ export default function CreateProjectsFooter({
         organization,
       });
       clearIndicators();
-      onComplete();
+      if (
+        isMobile() &&
+        organization.experiments.TargetedOnboardingMobileRedirectExperiment === 'hide'
+      ) {
+        if (shouldSendEmail) {
+          persistedOnboardingState &&
+            (await api.requestPromise(
+              `/organizations/${organization.slug}/onboarding-continuation-email/`,
+              {
+                method: 'POST',
+                data: {
+                  platforms: persistedOnboardingState.selectedPlatforms,
+                },
+              }
+            ));
+        }
+        browserHistory.push(`/onboarding/${organization.slug}/mobile-redirect/`);
+      } else {
+        onComplete();
+      }
     } catch (err) {
       addErrorMessage(t('Failed to create projects'));
       Sentry.captureException(err);

+ 27 - 0
static/app/views/onboarding/targetedOnboarding/components/firstEventFooter.tsx

@@ -12,7 +12,9 @@ import space from 'sentry/styles/space';
 import {Group, Organization, Project} from 'sentry/types';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import EventWaiter from 'sentry/utils/eventWaiter';
+import isMobile from 'sentry/utils/isMobile';
 import testableTransition from 'sentry/utils/testableTransition';
+import useApi from 'sentry/utils/useApi';
 import CreateSampleEventButton from 'sentry/views/onboarding/createSampleEventButton';
 
 import {usePersistedOnboardingState} from '../utils';
@@ -38,8 +40,33 @@ export default function FirstEventFooter({
 }: FirstEventFooterProps) {
   const source = 'targeted_onboarding_first_event_footer';
   const [clientState, setClientState] = usePersistedOnboardingState();
+  const client = useApi();
 
   const getSecondaryCta = () => {
+    if (
+      isMobile() &&
+      organization.experiments.TargetedOnboardingMobileRedirectExperiment === 'email-cta'
+    ) {
+      return (
+        <Button
+          to={`/onboarding/${organization.slug}/mobile-redirect/`}
+          onClick={() => {
+            clientState &&
+              client.requestPromise(
+                `/organizations/${organization.slug}/onboarding-continuation-email/`,
+                {
+                  method: 'POST',
+                  data: {
+                    platforms: clientState.selectedPlatforms,
+                  },
+                }
+              );
+          }}
+        >
+          {t('Do it Later')}
+        </Button>
+      );
+    }
     // if hasn't sent first event, allow skiping.
     // if last, no secondary cta
     if (!hasFirstEvent && !isLast) {

+ 45 - 0
static/app/views/onboarding/targetedOnboarding/mobileRedirect.tsx

@@ -0,0 +1,45 @@
+import styled from '@emotion/styled';
+
+import OnboardingPreview from 'sentry-images/spot/onboarding-preview.svg';
+
+import Button from 'sentry/components/button';
+import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
+
+export default function MobileRedirect() {
+  return (
+    <Wrapper>
+      <ActionImage src={OnboardingPreview} />
+      <h3>{t('End of the line...for now')}</h3>
+      <p>
+        Once you are back at your computer, click the link in the email to finish
+        onboarding.
+      </p>
+      <Button type="button" priority="primary" href="https://docs.sentry.io/">
+        {t('Read up on Sentry')}
+      </Button>
+    </Wrapper>
+  );
+}
+
+const Wrapper = styled('div')`
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+  flex-grow: 1;
+  justify-content: center;
+  align-items: center;
+  img {
+    margin-bottom: ${space(4)};
+  }
+  h3 {
+    margin-bottom: ${space(2)};
+  }
+  p {
+    margin-bottom: ${space(4)};
+  }
+`;
+
+const ActionImage = styled('img')`
+  height: 182px;
+`;

+ 25 - 14
static/app/views/onboarding/targetedOnboarding/onboarding.tsx

@@ -20,6 +20,7 @@ import withProjects from 'sentry/utils/withProjects';
 import PageCorners from 'sentry/views/onboarding/components/pageCorners';
 
 import Stepper from './components/stepper';
+import MobileRedirect from './mobileRedirect';
 import PlatformSelection from './platform';
 import SetupDocs from './setupDocs';
 import {StepDescriptor} from './types';
@@ -59,6 +60,14 @@ const ONBOARDING_STEPS: StepDescriptor[] = [
   },
 ];
 
+const MobileRedirectStep: StepDescriptor = {
+  id: 'setup-docs',
+  title: t('Install the Sentry SDK'),
+  Component: MobileRedirect,
+  hasFooter: true,
+  cornerVariant: 'top-left',
+};
+
 function Onboarding(props: Props) {
   const {
     organization,
@@ -73,7 +82,7 @@ function Onboarding(props: Props) {
     };
   }, []);
 
-  const stepObj = ONBOARDING_STEPS.find(({id}) => stepId === id);
+  let stepObj = ONBOARDING_STEPS.find(({id}) => stepId === id);
   const stepIndex = ONBOARDING_STEPS.findIndex(({id}) => stepId === id);
 
   const cornerVariantControl = useAnimation();
@@ -101,12 +110,6 @@ function Onboarding(props: Props) {
 
   useEffect(updateAnimationState, []);
 
-  if (!stepObj || stepIndex === -1) {
-    return (
-      <Redirect to={`/onboarding/${organization.slug}/${ONBOARDING_STEPS[0].id}/`} />
-    );
-  }
-
   const goToStep = (step: StepDescriptor) => {
     if (!stepObj) {
       return;
@@ -165,18 +168,26 @@ function Onboarding(props: Props) {
   };
 
   if (!stepObj || stepIndex === -1) {
-    return <div>Can't find</div>;
+    if (props.params.step === 'mobile-redirect') {
+      stepObj = MobileRedirectStep;
+    } else {
+      return (
+        <Redirect to={`/onboarding/${organization.slug}/${ONBOARDING_STEPS[0].id}/`} />
+      );
+    }
   }
   return (
     <OnboardingWrapper data-test-id="targeted-onboarding">
       <SentryDocumentTitle title={stepObj.title} />
       <Header>
         <LogoSvg />
-        <StyledStepper
-          numSteps={ONBOARDING_STEPS.length}
-          currentStepIndex={stepIndex}
-          onClick={i => goToStep(ONBOARDING_STEPS[i])}
-        />
+        {stepIndex !== -1 && (
+          <StyledStepper
+            numSteps={ONBOARDING_STEPS.length}
+            currentStepIndex={stepIndex}
+            onClick={i => goToStep(ONBOARDING_STEPS[i])}
+          />
+        )}
         <UpsellWrapper>
           <Hook
             name="onboarding:targeted-onboarding-header"
@@ -192,7 +203,7 @@ function Onboarding(props: Props) {
               <stepObj.Component
                 active
                 stepIndex={stepIndex}
-                onComplete={() => goNextStep(stepObj)}
+                onComplete={() => stepObj && goNextStep(stepObj)}
                 orgId={props.params.orgId}
                 organization={props.organization}
                 search={props.location.search}

+ 1 - 0
static/app/views/onboarding/targetedOnboarding/types.ts

@@ -31,6 +31,7 @@ export type OnboardingState = {
   // Contains platforms currently selected. This is different from `platforms` because
   // a project created by onboarding could be unselected by the user in the future.
   selectedPlatforms: PlatformKey[];
+  mobileEmailSent?: boolean;
   state?: 'started' | 'projects_selected' | 'finished' | 'skipped';
   url?: string;
 };

+ 1 - 0
static/app/views/onboarding/targetedOnboarding/utils.tsx

@@ -13,6 +13,7 @@ export function usePersistedOnboardingState(): [
     useMemo(() => {
       const onboardingState = state
         ? {
+            ...state,
             platformToProjectIdMap: state.platformToProjectIdMap || {},
             selectedPlatforms: state.selectedPlatforms || [],
           }