import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {ModalRenderProps} from 'sentry/actionCreators/modal'; import {Client} from 'sentry/api'; import Alert from 'sentry/components/alert'; import Button from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {DEFAULT_TOAST_DURATION} from 'sentry/constants'; import {t, tct} from 'sentry/locale'; import space from 'sentry/styles/space'; import {Organization, Project} from 'sentry/types'; import { AppStoreConnectStatusData, CustomRepoAppStoreConnect, } from 'sentry/types/debugFiles'; import {unexpectedErrorMessage} from 'sentry/utils/appStoreValidationErrorMessage'; import withApi from 'sentry/utils/withApi'; import StepOne from './stepOne'; import StepTwo from './stepTwo'; import {AppStoreApp, StepOneData, StepTwoData} from './types'; import {getAppStoreErrorMessage} from './utils'; type Props = Pick & { api: Client; onSubmit: () => void; orgSlug: Organization['slug']; projectSlug: Project['slug']; appStoreConnectStatusData?: AppStoreConnectStatusData; initialData?: CustomRepoAppStoreConnect; }; const steps = [t('App Store Connect credentials'), t('Choose an application')]; function AppStoreConnect({ Header, Body, Footer, api, initialData, orgSlug, projectSlug, onSubmit, appStoreConnectStatusData, }: Props) { const {credentials} = appStoreConnectStatusData ?? {}; const [isLoading, setIsLoading] = useState(false); const [activeStep, setActiveStep] = useState(0); const [appStoreApps, setAppStoreApps] = useState([]); const [stepOneData, setStepOneData] = useState({ issuer: initialData?.appconnectIssuer, keyId: initialData?.appconnectKey, privateKey: typeof initialData?.appconnectPrivateKey === 'object' ? undefined : '', errors: undefined, }); const [stepTwoData, setStepTwoData] = useState({ app: undefined, }); async function checkCredentials() { setIsLoading(true); try { const response = await api.requestPromise( `/projects/${orgSlug}/${projectSlug}/appstoreconnect/apps/`, { method: 'POST', data: { id: stepOneData.privateKey !== undefined ? undefined : initialData?.id, appconnectIssuer: stepOneData.issuer, appconnectKey: stepOneData.keyId, appconnectPrivateKey: stepOneData.privateKey, }, } ); const storeApps: AppStoreApp[] = response.apps; if (!!initialData && !storeApps.find(app => app.appId === initialData.appId)) { addErrorMessage(t('Credentials not authorized for this application')); setIsLoading(false); return; } setAppStoreApps(storeApps); if ( stepTwoData.app?.appId && !storeApps.find(app => app.appId === stepTwoData.app?.appId) ) { setStepTwoData({app: storeApps[0]}); } if (!!initialData) { updateCredentials(); return; } setIsLoading(false); goNext(); } catch (error) { setIsLoading(false); const appStoreConnnectError = getAppStoreErrorMessage(error); if (typeof appStoreConnnectError === 'string') { // app-connect-authentication-error // app-connect-forbidden-error addErrorMessage(appStoreConnnectError); return; } setStepOneData({...stepOneData, errors: appStoreConnnectError}); } } function closeModal() { setTimeout(() => onSubmit(), DEFAULT_TOAST_DURATION); } async function updateCredentials() { if (!initialData) { return; } try { await api.requestPromise( `/projects/${orgSlug}/${projectSlug}/appstoreconnect/${initialData.id}/`, { method: 'POST', data: { appconnectIssuer: stepOneData.issuer, appconnectKey: stepOneData.keyId, appconnectPrivateKey: stepOneData.privateKey, appName: initialData.appName, appId: initialData.appId, bundleId: initialData.bundleId, }, } ); addSuccessMessage(t('Successfully updated custom repository')); closeModal(); } catch (error) { setIsLoading(false); const appStoreConnnectError = getAppStoreErrorMessage(error); if (typeof appStoreConnnectError === 'string') { if (appStoreConnnectError === unexpectedErrorMessage) { addErrorMessage(t('An error occurred while updating the custom repository')); return; } addErrorMessage(appStoreConnnectError); } } } async function persistData() { if (!stepTwoData.app) { return; } setIsLoading(true); try { await api.requestPromise(`/projects/${orgSlug}/${projectSlug}/appstoreconnect/`, { method: 'POST', data: { appconnectIssuer: stepOneData.issuer, appconnectKey: stepOneData.keyId, appconnectPrivateKey: stepOneData.privateKey, appName: stepTwoData.app.name, appId: stepTwoData.app.appId, bundleId: stepTwoData.app.bundleId, }, }); addSuccessMessage(t('Successfully added custom repository')); closeModal(); } catch (error) { setIsLoading(false); const appStoreConnnectError = getAppStoreErrorMessage(error); if (typeof appStoreConnnectError === 'string') { if (appStoreConnnectError === unexpectedErrorMessage) { addErrorMessage(t('An error occurred while adding the custom repository')); return; } addErrorMessage(appStoreConnnectError); } } } function isFormInvalid() { switch (activeStep) { case 0: return Object.keys(stepOneData).some(key => { if (key === 'errors') { const errors = stepOneData[key] ?? {}; return Object.keys(errors).some(error => !!errors[error]); } if (key === 'privateKey' && stepOneData[key] === undefined) { return false; } return !stepOneData[key]; }); case 1: return Object.keys(stepTwoData).some(key => !stepTwoData[key]); default: return false; } } function goNext() { setActiveStep(activeStep + 1); } function handleGoBack() { const newActiveStep = activeStep - 1; setActiveStep(newActiveStep); } function handleGoNext() { switch (activeStep) { case 0: checkCredentials(); break; case 1: persistData(); break; default: break; } } function renderCurrentStep() { switch (activeStep) { case 0: return ; case 1: return ( ); default: return ( {t('This step could not be found.')} ); } } function getAlerts() { const alerts: React.ReactElement[] = []; if (activeStep !== 0) { return alerts; } if (credentials?.status === 'invalid') { alerts.push( {credentials.code === 'app-connect-forbidden-error' ? t( 'Your App Store Connect credentials have insufficient permissions. To reconnect, update your credentials.' ) : t( 'Your App Store Connect credentials are invalid. To reconnect, update your credentials.' )} ); } return alerts; } function renderBodyContent() { const alerts = getAlerts(); return ( {!!alerts.length && ( {alerts.map((alert, index) => ( {alert} ))} )} {renderCurrentStep()} ); } if (initialData && !appStoreConnectStatusData) { return ; } return (
{activeStep + 1} {steps[activeStep]} {tct('[currentStep] of [totalSteps]', { currentStep: activeStep + 1, totalSteps: !!initialData ? 1 : steps.length, })}
{renderBodyContent()}
); } export default withApi(AppStoreConnect); const HeaderContent = styled('div')` display: grid; grid-template-columns: max-content max-content 1fr; align-items: center; gap: ${space(1)}; `; const NumericSymbol = styled('div')` border-radius: 50%; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; font-weight: 700; font-size: ${p => p.theme.fontSizeMedium}; background-color: ${p => p.theme.yellow300}; `; const HeaderContentTitle = styled('div')` font-weight: 700; font-size: ${p => p.theme.fontSizeExtraLarge}; `; const StepsOverview = styled('div')` color: ${p => p.theme.gray300}; display: flex; justify-content: flex-end; `; const LoadingIndicatorWrapper = styled('div')` height: 100%; position: absolute; width: 100%; top: 0; left: 0; display: flex; align-items: center; justify-content: center; `; const StyledButton = styled(Button)` position: relative; `; const Alerts = styled('div')` display: grid; gap: ${space(1.5)}; margin-bottom: ${space(3)}; `; const StyledAlert = styled(Alert)` margin: 0; `;