Browse Source

ref(app-store-connect): Add adjusts (#29843)

Priscila Oliveira 3 years ago
parent
commit
2749e902bc

+ 0 - 1
src/sentry/utils/prompts.py

@@ -5,7 +5,6 @@ DEFAULT_PROMPTS = {
     "releases": {"required_fields": ["organization_id", "project_id"]},
     "suspect_commits": {"required_fields": ["organization_id", "project_id"]},
     "alert_stream": {"required_fields": ["organization_id"]},
-    "app_store_connect_updates": {"required_fields": ["organization_id", "project_id"]},
     "sdk_updates": {"required_fields": ["organization_id"]},
     "suggest_mobile_project": {"required_fields": ["organization_id"]},
     "stacktrace_link": {"required_fields": ["organization_id", "project_id"]},

+ 2 - 3
static/app/actionCreators/modal.tsx

@@ -7,9 +7,8 @@ import {DashboardWidgetLibraryModalOptions} from 'app/components/modals/dashboar
 import type {DashboardWidgetQuerySelectorModalOptions} from 'app/components/modals/dashboardWidgetQuerySelectorModal';
 import {InviteRow} from 'app/components/modals/inviteMembersModal/types';
 import type {ReprocessEventModalOptions} from 'app/components/modals/reprocessEventModal';
-import {AppStoreConnectContextProps} from 'app/components/projects/appStoreConnectContext';
 import {Group, IssueOwnership, Organization, Project, SentryApp, Team} from 'app/types';
-import {CustomRepoType} from 'app/types/debugFiles';
+import {AppStoreConnectStatusData, CustomRepoType} from 'app/types/debugFiles';
 import {Event} from 'app/types/event';
 
 export type ModalOptions = ModalTypes['options'];
@@ -197,7 +196,7 @@ export type SentryAppDetailsModalOptions = {
 type DebugFileSourceModalOptions = {
   sourceType: CustomRepoType;
   onSave: (data: Record<string, any>) => Promise<void>;
-  appStoreConnectContext?: AppStoreConnectContextProps;
+  appStoreConnectStatusData?: AppStoreConnectStatusData;
   onClose?: () => void;
   sourceConfig?: Record<string, any>;
 };

+ 1 - 0
static/app/actionCreators/prompts.tsx

@@ -76,6 +76,7 @@ export async function promptsCheck(
   const response: PromptResponse = await api.requestPromise('/prompts-activity/', {
     query,
   });
+
   const data = response?.data;
 
   if (!data) {

+ 31 - 128
static/app/components/globalAppStoreConnectUpdateAlert/updateAlert.tsx

@@ -1,24 +1,13 @@
-import {Fragment, useContext, useEffect, useState} from 'react';
+import {useContext} from 'react';
 import styled from '@emotion/styled';
 
-import {promptsCheck, promptsUpdate} from 'app/actionCreators/prompts';
-import {Client} from 'app/api';
 import Alert from 'app/components/alert';
-import Button from 'app/components/button';
-import Link from 'app/components/links/link';
 import AppStoreConnectContext from 'app/components/projects/appStoreConnectContext';
-import {IconClose, IconRefresh} from 'app/icons';
-import {t} from 'app/locale';
+import {IconRefresh} from 'app/icons';
 import space from 'app/styles/space';
 import {Organization, Project} from 'app/types';
-import {AppStoreConnectStatusData} from 'app/types/debugFiles';
-import {promptIsDismissed} from 'app/utils/promptIsDismissed';
-import withApi from 'app/utils/withApi';
-
-const APP_STORE_CONNECT_UPDATES = 'app_store_connect_updates';
 
 type Props = {
-  api: Client;
   organization: Organization;
   project?: Project;
   Wrapper?: React.ComponentType;
@@ -26,139 +15,53 @@ type Props = {
   className?: string;
 };
 
-function UpdateAlert({api, Wrapper, isCompact, project, organization, className}: Props) {
+function UpdateAlert({Wrapper, project, className}: Props) {
   const appStoreConnectContext = useContext(AppStoreConnectContext);
-  const [isDismissed, setIsDismissed] = useState(false);
-
-  useEffect(() => {
-    checkPrompt();
-  }, []);
-
-  async function checkPrompt() {
-    if (
-      !project ||
-      !appStoreConnectContext ||
-      !appStoreConnectContext.updateAlertMessage ||
-      isDismissed
-    ) {
-      return;
-    }
-
-    const prompt = await promptsCheck(api, {
-      organizationId: organization.id,
-      projectId: project.id,
-      feature: APP_STORE_CONNECT_UPDATES,
-    });
-
-    setIsDismissed(promptIsDismissed(prompt));
-  }
-
-  function handleDismiss() {
-    if (!project) {
-      return;
-    }
-
-    promptsUpdate(api, {
-      organizationId: organization.id,
-      projectId: project.id,
-      feature: APP_STORE_CONNECT_UPDATES,
-      status: 'dismissed',
-    });
-
-    setIsDismissed(true);
-  }
-
-  function renderMessage(
-    appStoreConnectStatusData: AppStoreConnectStatusData,
-    projectSettingsLink: string
-  ) {
-    if (!appStoreConnectStatusData.updateAlertMessage) {
-      return null;
-    }
-
-    const {updateAlertMessage} = appStoreConnectStatusData;
-
-    return (
-      <div>
-        {updateAlertMessage}
-        {isCompact && (
-          <Fragment>
-            &nbsp;
-            <Link to={projectSettingsLink}>
-              {t('Update it in the project settings to reconnect')}
-            </Link>
-          </Fragment>
-        )}
-      </div>
-    );
-  }
-
-  function renderActions(projectSettingsLink: string) {
-    if (isCompact) {
-      return (
-        <ButtonClose
-          priority="link"
-          title={t('Dismiss')}
-          label={t('Dismiss')}
-          onClick={handleDismiss}
-          icon={<IconClose />}
-        />
-      );
-    }
-
-    return (
-      <Actions>
-        <Button priority="link" onClick={handleDismiss}>
-          {t('Dismiss')}
-        </Button>
-        |
-        <Button priority="link" to={projectSettingsLink}>
-          {t('Update session')}
-        </Button>
-      </Actions>
-    );
-  }
 
   if (
     !project ||
     !appStoreConnectContext ||
-    !appStoreConnectContext.updateAlertMessage ||
-    isDismissed
+    !Object.keys(appStoreConnectContext).some(
+      key => !!appStoreConnectContext[key].updateAlertMessage
+    )
   ) {
     return null;
   }
 
-  const projectSettingsLink = `/settings/${organization.slug}/projects/${project.slug}/debug-symbols/?customRepository=${appStoreConnectContext.id}`;
-
-  const notice = (
-    <Alert type="warning" icon={<IconRefresh />} className={className}>
-      <Content>
-        {renderMessage(appStoreConnectContext, projectSettingsLink)}
-        {renderActions(projectSettingsLink)}
-      </Content>
-    </Alert>
+  const notices = (
+    <Notices className={className}>
+      {Object.keys(appStoreConnectContext).map(key => {
+        const {updateAlertMessage} = appStoreConnectContext[key];
+        if (!updateAlertMessage) {
+          return null;
+        }
+
+        return (
+          <NoMarginBottomAlert key={key} type="warning" icon={<IconRefresh />}>
+            <AlertContent>{updateAlertMessage}</AlertContent>
+          </NoMarginBottomAlert>
+        );
+      })}
+    </Notices>
   );
 
-  return Wrapper ? <Wrapper>{notice}</Wrapper> : notice;
+  return Wrapper ? <Wrapper>{notices}</Wrapper> : notices;
 }
 
-export default withApi(UpdateAlert);
+export default UpdateAlert;
 
-const Actions = styled('div')`
+const Notices = styled('div')`
   display: grid;
-  grid-template-columns: repeat(3, max-content);
-  grid-gap: ${space(1)};
-  align-items: center;
+  grid-gap: ${space(2)};
+  margin-bottom: ${space(3)};
+`;
+
+const NoMarginBottomAlert = styled(Alert)`
+  margin-bottom: 0;
 `;
 
-const Content = styled('div')`
+const AlertContent = styled('div')`
   display: grid;
   grid-template-columns: 1fr max-content;
   grid-gap: ${space(1)};
 `;
-
-const ButtonClose = styled(Button)`
-  color: ${p => p.theme.textColor};
-  /* Give the button an explicit height so that it lines up with the icon */
-  height: 22px;
-`;

+ 8 - 6
static/app/components/modals/debugFileCustomRepository/appStoreConnect/index.tsx

@@ -8,17 +8,18 @@ import Alert from 'app/components/alert';
 import Button from 'app/components/button';
 import ButtonBar from 'app/components/buttonBar';
 import LoadingIndicator from 'app/components/loadingIndicator';
-import {AppStoreConnectContextProps} from 'app/components/projects/appStoreConnectContext';
 import {IconWarning} from 'app/icons';
 import {t, tct} from 'app/locale';
 import space from 'app/styles/space';
 import {Organization, Project} from 'app/types';
+import {AppStoreConnectStatusData} from 'app/types/debugFiles';
+import {unexpectedErrorMessage} from 'app/utils/appStoreValidationErrorMessage';
 import withApi from 'app/utils/withApi';
 
 import StepOne from './stepOne';
 import StepTwo from './stepTwo';
 import {AppStoreApp, StepOneData, StepTwoData} from './types';
-import {getAppStoreErrorMessage, unexpectedErrorMessage} from './utils';
+import {getAppStoreErrorMessage} from './utils';
 
 type InitialData = {
   type: string;
@@ -39,7 +40,7 @@ type Props = Pick<ModalRenderProps, 'Header' | 'Body' | 'Footer'> & {
   orgSlug: Organization['slug'];
   projectSlug: Project['slug'];
   onSubmit: () => void;
-  appStoreConnectContext?: AppStoreConnectContextProps;
+  appStoreConnectStatusData?: AppStoreConnectStatusData;
   initialData?: InitialData;
 };
 
@@ -54,9 +55,9 @@ function AppStoreConnect({
   orgSlug,
   projectSlug,
   onSubmit,
-  appStoreConnectContext,
+  appStoreConnectStatusData,
 }: Props) {
-  const {credentials} = appStoreConnectContext ?? {};
+  const {credentials} = appStoreConnectStatusData ?? {};
 
   const [isLoading, setIsLoading] = useState(false);
   const [activeStep, setActiveStep] = useState(0);
@@ -238,6 +239,7 @@ function AppStoreConnect({
     if (activeStep !== 0) {
       return alerts;
     }
+
     if (credentials?.status === 'invalid') {
       alerts.push(
         <StyledAlert type="warning" icon={<IconWarning />}>
@@ -272,7 +274,7 @@ function AppStoreConnect({
     );
   }
 
-  if (initialData && !appStoreConnectContext) {
+  if (initialData && !appStoreConnectStatusData) {
     return <LoadingIndicator />;
   }
 

+ 7 - 36
static/app/components/modals/debugFileCustomRepository/appStoreConnect/utils.tsx

@@ -1,6 +1,10 @@
 import * as Sentry from '@sentry/react';
 
 import {t} from 'app/locale';
+import {
+  getAppStoreValidationErrorMessage,
+  unexpectedErrorMessage,
+} from 'app/utils/appStoreValidationErrorMessage';
 
 import {StepOneData} from './types';
 
@@ -28,23 +32,14 @@ const fieldErrorMessageMapping = {
   },
 };
 
-export type ErrorCodeDetailed =
-  | 'app-connect-authentication-error'
-  | 'app-connect-forbidden-error'
-  | 'app-connect-multiple-sources-error';
-
-export type ValidationErrorDetailed = {
-  code: ErrorCodeDetailed;
-};
-
 type ResponseJSONDetailed = {
-  detail: ValidationErrorDetailed & {
+  detail: Parameters<typeof getAppStoreValidationErrorMessage>[0] & {
     extra: Record<string, any>;
     message: string;
   };
 };
 
-export type AppStoreConnectField = keyof typeof fieldErrorMessageMapping;
+type AppStoreConnectField = keyof typeof fieldErrorMessageMapping;
 
 type ResponseJSON = Record<AppStoreConnectField, string[]>;
 
@@ -53,10 +48,6 @@ type Error = {
   responseJSON?: ResponseJSON | ResponseJSONDetailed;
 };
 
-export const unexpectedErrorMessage = t(
-  'An unexpected error occurred while configuring the App Store Connect integration'
-);
-
 export function getAppStoreErrorMessage(
   error: Error | string
 ): string | Record<keyof StepOneData, string> {
@@ -68,7 +59,7 @@ export function getAppStoreErrorMessage(
     ?.detail;
 
   if (detailedErrorResponse) {
-    return getAppStoreValidationErrorMessage(detailedErrorResponse);
+    return getAppStoreValidationErrorMessage(detailedErrorResponse) as string;
   }
 
   const errorResponse = error.responseJSON as undefined | ResponseJSON;
@@ -106,23 +97,3 @@ export function getAppStoreErrorMessage(
     {}
   ) as Record<keyof StepOneData, string>;
 }
-
-export function getAppStoreValidationErrorMessage(
-  error: ValidationErrorDetailed
-): string {
-  switch (error.code) {
-    case 'app-connect-authentication-error':
-      return t(
-        'Credentials are invalid or missing. Check the entered App Store Connect credentials are correct.'
-      );
-    case 'app-connect-forbidden-error':
-      return t('The supplied API key does not have sufficient permissions.');
-    case 'app-connect-multiple-sources-error':
-      return t('Only one Apple App Store Connect application is allowed in this project');
-    default: {
-      // this shall not happen
-      Sentry.captureException(new Error('Unknown app store connect error'));
-      return unexpectedErrorMessage;
-    }
-  }
-}

+ 4 - 5
static/app/components/modals/debugFileCustomRepository/index.tsx

@@ -3,10 +3,9 @@ import {withRouter, WithRouterProps} from 'react-router';
 import {css} from '@emotion/react';
 
 import {ModalRenderProps} from 'app/actionCreators/modal';
-import {AppStoreConnectContextProps} from 'app/components/projects/appStoreConnectContext';
 import {getDebugSourceName} from 'app/data/debugFileSources';
 import {tct} from 'app/locale';
-import {CustomRepoType} from 'app/types/debugFiles';
+import {AppStoreConnectStatusData, CustomRepoType} from 'app/types/debugFiles';
 import FieldFromConfig from 'app/views/settings/components/forms/fieldFromConfig';
 import Form from 'app/views/settings/components/forms/form';
 
@@ -35,7 +34,7 @@ type Props = WithRouterProps<RouteParams, {}> & {
    */
   sourceType: CustomRepoType;
 
-  appStoreConnectContext?: AppStoreConnectContextProps;
+  appStoreConnectStatusData?: AppStoreConnectStatusData;
   /**
    * The sourceConfig. May be empty to create a new one.
    */
@@ -50,7 +49,7 @@ function DebugFileCustomRepository({
   sourceConfig,
   sourceType,
   params: {orgId, projectId: projectSlug},
-  appStoreConnectContext,
+  appStoreConnectStatusData,
   closeModal,
 }: Props) {
   function handleSave(data?: Record<string, any>) {
@@ -75,7 +74,7 @@ function DebugFileCustomRepository({
         projectSlug={projectSlug}
         onSubmit={handleSave}
         initialData={sourceConfig as AppStoreConnectInitialData}
-        appStoreConnectContext={appStoreConnectContext}
+        appStoreConnectStatusData={appStoreConnectStatusData}
       />
     );
   }

+ 131 - 0
static/app/components/projects/appStoreConnectContext.tsx

@@ -0,0 +1,131 @@
+import {createContext, useEffect, useState} from 'react';
+
+import {Organization, Project} from 'app/types';
+import {
+  AppStoreConnectCredentialsStatus,
+  AppStoreConnectStatusData,
+} from 'app/types/debugFiles';
+import {getAppStoreValidationErrorMessage} from 'app/utils/appStoreValidationErrorMessage';
+import useApi from 'app/utils/useApi';
+
+export type AppStoreConnectContextProps =
+  | Record<string, AppStoreConnectStatusData>
+  | undefined;
+
+const AppStoreConnectContext = createContext<AppStoreConnectContextProps>(undefined);
+
+type ProviderProps = {
+  children: React.ReactNode;
+  organization: Organization;
+  project?: Project;
+};
+
+const Provider = ({children, project, organization}: ProviderProps) => {
+  const api = useApi();
+
+  const [projectDetails, setProjectDetails] = useState<undefined | Project>();
+  const [appStoreConnectStatusData, setAppStoreConnectStatusData] =
+    useState<AppStoreConnectContextProps>(undefined);
+
+  const orgSlug = organization.slug;
+
+  const appStoreConnectSymbolSources = (
+    projectDetails?.symbolSources ? JSON.parse(projectDetails.symbolSources) : []
+  ).reduce((acc, {type, id, ...symbolSource}) => {
+    if (type.toLowerCase() === 'appstoreconnect') {
+      acc[id] = {type, ...symbolSource};
+    }
+    return acc;
+  }, {});
+
+  useEffect(() => {
+    fetchProjectDetails();
+  }, [project]);
+
+  useEffect(() => {
+    fetchAppStoreConnectStatusData();
+  }, [projectDetails]);
+
+  async function fetchProjectDetails() {
+    if (!project || projectDetails) {
+      return;
+    }
+
+    if (project.symbolSources) {
+      setProjectDetails(project);
+      return;
+    }
+
+    try {
+      const response = await api.requestPromise(`/projects/${orgSlug}/${project.slug}/`);
+      setProjectDetails(response);
+    } catch {
+      // do nothing
+    }
+  }
+
+  async function fetchAppStoreConnectStatusData() {
+    if (!projectDetails) {
+      return;
+    }
+
+    if (!Object.keys(appStoreConnectSymbolSources).length) {
+      return;
+    }
+
+    try {
+      const response: Record<string, AppStoreConnectStatusData> =
+        await api.requestPromise(
+          `/projects/${orgSlug}/${projectDetails.slug}/appstoreconnect/status/`
+        );
+
+      setAppStoreConnectStatusData(response);
+    } catch {
+      // do nothing
+    }
+  }
+
+  function getUpdateAlertMessage(
+    respository: NonNullable<Parameters<typeof getAppStoreValidationErrorMessage>[1]>,
+    credentials: AppStoreConnectCredentialsStatus
+  ) {
+    if (credentials?.status === 'valid') {
+      return undefined;
+    }
+
+    return getAppStoreValidationErrorMessage(credentials, respository);
+  }
+
+  return (
+    <AppStoreConnectContext.Provider
+      value={
+        appStoreConnectStatusData && project
+          ? Object.keys(appStoreConnectStatusData).reduce((acc, key) => {
+              const appStoreConnect = appStoreConnectStatusData[key];
+              return {
+                ...acc,
+                [key]: {
+                  ...appStoreConnect,
+                  updateAlertMessage: getUpdateAlertMessage(
+                    {
+                      name: appStoreConnectSymbolSources[key].name,
+                      link: `/settings/${organization.slug}/projects/${project.slug}/debug-symbols/?customRepository=${key}`,
+                    },
+                    appStoreConnect.credentials
+                  ),
+                },
+              };
+            }, {})
+          : undefined
+      }
+    >
+      {children}
+    </AppStoreConnectContext.Provider>
+  );
+};
+
+const Consumer = AppStoreConnectContext.Consumer;
+
+export {Provider, Consumer};
+
+export default AppStoreConnectContext;

+ 0 - 113
static/app/components/projects/appStoreConnectContext/index.tsx

@@ -1,113 +0,0 @@
-import {createContext, useEffect, useState} from 'react';
-
-import {Organization, Project} from 'app/types';
-import {AppStoreConnectStatusData} from 'app/types/debugFiles';
-import useApi from 'app/utils/useApi';
-
-export type AppStoreConnectContextProps = AppStoreConnectStatusData | undefined;
-
-const AppStoreConnectContext = createContext<AppStoreConnectContextProps>(undefined);
-
-import {getAppConnectStoreUpdateAlertMessage} from './utils';
-
-type ProviderProps = {
-  children: React.ReactNode;
-  organization: Organization;
-  project?: Project;
-};
-
-const Provider = ({children, project, organization}: ProviderProps) => {
-  const api = useApi();
-
-  const [projectDetails, setProjectDetails] = useState<undefined | Project>();
-  const [appStoreConnectStatusData, setAppStoreConnectStatusData] =
-    useState<AppStoreConnectContextProps>(undefined);
-
-  const orgSlug = organization.slug;
-
-  useEffect(() => {
-    fetchProjectDetails();
-  }, [project]);
-
-  useEffect(() => {
-    fetchAppStoreConnectStatusData();
-  }, [projectDetails]);
-
-  async function fetchProjectDetails() {
-    if (!project || projectDetails) {
-      return;
-    }
-
-    if (project.symbolSources) {
-      setProjectDetails(project);
-      return;
-    }
-
-    try {
-      const response = await api.requestPromise(`/projects/${orgSlug}/${project.slug}/`);
-      setProjectDetails(response);
-    } catch {
-      // do nothing
-    }
-  }
-
-  function getAppStoreConnectSymbolSourceId(symbolSources?: string) {
-    return (symbolSources ? JSON.parse(symbolSources) : []).find(
-      symbolSource => symbolSource.type.toLowerCase() === 'appstoreconnect'
-    )?.id;
-  }
-
-  async function fetchAppStoreConnectStatusData() {
-    if (!projectDetails) {
-      return;
-    }
-
-    const appStoreConnectSymbolSourceId = getAppStoreConnectSymbolSourceId(
-      projectDetails.symbolSources
-    );
-
-    if (!appStoreConnectSymbolSourceId) {
-      return;
-    }
-
-    try {
-      const response: Map<string, AppStoreConnectStatusData> = await api.requestPromise(
-        `/projects/${orgSlug}/${projectDetails.slug}/appstoreconnect/status/`
-      );
-
-      const sourceStatus: Omit<AppStoreConnectStatusData, 'id'> | undefined =
-        response[appStoreConnectSymbolSourceId];
-      if (sourceStatus) {
-        setAppStoreConnectStatusData({
-          ...sourceStatus,
-          id: appStoreConnectSymbolSourceId,
-        });
-      }
-    } catch {
-      // do nothing
-    }
-  }
-
-  return (
-    <AppStoreConnectContext.Provider
-      value={
-        appStoreConnectStatusData
-          ? {
-              ...appStoreConnectStatusData,
-              updateAlertMessage: getAppConnectStoreUpdateAlertMessage(
-                appStoreConnectStatusData.credentials
-              ),
-            }
-          : undefined
-      }
-    >
-      {children}
-    </AppStoreConnectContext.Provider>
-  );
-};
-
-const Consumer = AppStoreConnectContext.Consumer;
-
-export {Provider, Consumer};
-
-export default AppStoreConnectContext;

+ 0 - 14
static/app/components/projects/appStoreConnectContext/utils.tsx

@@ -1,14 +0,0 @@
-import {
-  getAppStoreValidationErrorMessage,
-  ValidationErrorDetailed,
-} from 'app/components/modals/debugFileCustomRepository/appStoreConnect/utils';
-import {AppStoreConnectCredentialsStatus} from 'app/types/debugFiles';
-
-export function getAppConnectStoreUpdateAlertMessage(
-  credentialsStatus: AppStoreConnectCredentialsStatus
-) {
-  if (credentialsStatus?.status === 'valid') {
-    return undefined;
-  }
-  return getAppStoreValidationErrorMessage(credentialsStatus as ValidationErrorDetailed);
-}

Some files were not shown because too many files changed in this diff