index.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import {Fragment, useContext, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import isEqual from 'lodash/isEqual';
  4. import {addErrorMessage, addSuccessMessage} from 'app/actionCreators/indicator';
  5. import {ModalRenderProps} from 'app/actionCreators/modal';
  6. import {Client} from 'app/api';
  7. import Alert from 'app/components/alert';
  8. import Button from 'app/components/button';
  9. import ButtonBar from 'app/components/buttonBar';
  10. import List from 'app/components/list';
  11. import {IconWarning} from 'app/icons';
  12. import {t} from 'app/locale';
  13. import space from 'app/styles/space';
  14. import {Organization, Project} from 'app/types';
  15. import withApi from 'app/utils/withApi';
  16. import AppStoreConnectContext from 'app/views/settings/project/appStoreConnectContext';
  17. import Accordion from './accordion';
  18. import AppStoreCredentials from './appStoreCredentials';
  19. import ItunesCredentials from './itunesCredentials';
  20. import {AppStoreCredentialsData, ItunesCredentialsData} from './types';
  21. type IntialData = {
  22. appId: string;
  23. appName: string;
  24. appconnectIssuer: string;
  25. appconnectKey: string;
  26. encrypted: string;
  27. id: string;
  28. itunesUser: string;
  29. name: string;
  30. orgId: number;
  31. orgName: string;
  32. type: string;
  33. };
  34. type Props = Pick<ModalRenderProps, 'Body' | 'Footer' | 'closeModal'> & {
  35. api: Client;
  36. orgSlug: Organization['slug'];
  37. projectSlug: Project['slug'];
  38. onSubmit: (data: Record<string, any>) => void;
  39. initialData?: IntialData;
  40. };
  41. function AppStoreConnect({
  42. Body,
  43. Footer,
  44. closeModal,
  45. api,
  46. initialData,
  47. orgSlug,
  48. projectSlug,
  49. onSubmit,
  50. }: Props) {
  51. const appStoreConnectContext = useContext(AppStoreConnectContext);
  52. const [isLoading, setIsLoading] = useState(false);
  53. const appStoreCredentialsInitialData = {
  54. issuer: initialData?.appconnectIssuer,
  55. keyId: initialData?.appconnectKey,
  56. privateKey: undefined,
  57. app:
  58. initialData?.appName && initialData?.appId
  59. ? {
  60. appId: initialData.appId,
  61. name: initialData.appName,
  62. }
  63. : undefined,
  64. };
  65. const iTunesCredentialsInitialData = {
  66. username: initialData?.itunesUser,
  67. password: undefined,
  68. authenticationCode: undefined,
  69. org:
  70. initialData?.orgId && initialData?.orgName
  71. ? {
  72. organizationId: initialData.orgId,
  73. name: initialData.appName,
  74. }
  75. : undefined,
  76. useSms: undefined,
  77. sessionContext: undefined,
  78. };
  79. const [
  80. appStoreCredentialsData,
  81. setAppStoreCredentialsData,
  82. ] = useState<AppStoreCredentialsData>(appStoreCredentialsInitialData);
  83. const [
  84. iTunesCredentialsData,
  85. setItunesCredentialsData,
  86. ] = useState<ItunesCredentialsData>(iTunesCredentialsInitialData);
  87. async function handleSave() {
  88. let endpoint = `/projects/${orgSlug}/${projectSlug}/appstoreconnect/`;
  89. let successMessage = t('App Store Connect repository was successfully added');
  90. let errorMessage = t(
  91. 'An error occured while adding the App Store Connect repository'
  92. );
  93. if (!!initialData) {
  94. endpoint = `${endpoint}${initialData.id}/`;
  95. successMessage = t('App Store Connect repository was successfully updated');
  96. errorMessage = t(
  97. 'An error occured while updating the App Store Connect repository'
  98. );
  99. }
  100. setIsLoading(true);
  101. try {
  102. const response = await api.requestPromise(endpoint, {
  103. method: 'POST',
  104. data: {
  105. appconnectIssuer: appStoreCredentialsData.issuer,
  106. appconnectKey: appStoreCredentialsData.keyId,
  107. appconnectPrivateKey: appStoreCredentialsData.privateKey,
  108. appName: appStoreCredentialsData.app?.name,
  109. appId: appStoreCredentialsData.app?.appId,
  110. itunesUser: iTunesCredentialsData.username,
  111. itunesPassword: iTunesCredentialsData.password,
  112. orgId: iTunesCredentialsData.org?.organizationId,
  113. orgName: iTunesCredentialsData.org?.name,
  114. sessionContext: iTunesCredentialsData.sessionContext,
  115. },
  116. });
  117. addSuccessMessage(successMessage);
  118. setIsLoading(false);
  119. onSubmit(response);
  120. closeModal();
  121. } catch {
  122. setIsLoading(false);
  123. addErrorMessage(errorMessage);
  124. }
  125. }
  126. const isUpdating = !!initialData;
  127. function isDataInvalid(data: Record<string, any>) {
  128. return Object.keys(data).some(key => {
  129. const value = data[key];
  130. if (typeof value === 'string') {
  131. return !value.trim();
  132. }
  133. return typeof value === 'undefined';
  134. });
  135. }
  136. function isAppStoreCredentialsDataInvalid() {
  137. return isDataInvalid(appStoreCredentialsData);
  138. }
  139. function isItunesCredentialsDataInvalid() {
  140. return isDataInvalid(iTunesCredentialsData);
  141. }
  142. function isFormInvalid() {
  143. if (!!initialData) {
  144. const isAppStoreCredentialsDataTheSame = isEqual(
  145. appStoreCredentialsData,
  146. appStoreCredentialsInitialData
  147. );
  148. const isItunesCredentialsDataTheSame = isEqual(
  149. iTunesCredentialsData,
  150. iTunesCredentialsInitialData
  151. );
  152. if (!isAppStoreCredentialsDataTheSame && !isItunesCredentialsDataTheSame) {
  153. return isAppStoreCredentialsDataInvalid() && isItunesCredentialsDataInvalid();
  154. }
  155. if (!isAppStoreCredentialsDataTheSame) {
  156. return isAppStoreCredentialsDataInvalid();
  157. }
  158. if (!isItunesCredentialsDataTheSame) {
  159. return isItunesCredentialsDataInvalid();
  160. }
  161. return isAppStoreCredentialsDataTheSame && isItunesCredentialsDataTheSame;
  162. }
  163. return isAppStoreCredentialsDataInvalid() && isItunesCredentialsDataInvalid();
  164. }
  165. return (
  166. <Fragment>
  167. <Body>
  168. <StyledList symbol="colored-numeric">
  169. <Accordion
  170. summary={t('App Store Connect credentials')}
  171. defaultExpanded={
  172. !isUpdating || !!appStoreConnectContext?.appstoreCredentialsValid
  173. }
  174. >
  175. {!!appStoreConnectContext?.appstoreCredentialsValid && (
  176. <StyledAlert type="warning" icon={<IconWarning />}>
  177. {t(
  178. 'Your App Store Connect credentials are invalid. To reconnect, update your credentials'
  179. )}
  180. </StyledAlert>
  181. )}
  182. <AppStoreCredentials
  183. api={api}
  184. orgSlug={orgSlug}
  185. projectSlug={projectSlug}
  186. data={appStoreCredentialsData}
  187. onChange={setAppStoreCredentialsData}
  188. onReset={() => setAppStoreCredentialsData(appStoreCredentialsInitialData)}
  189. isUpdating={isUpdating}
  190. />
  191. </Accordion>
  192. <Accordion
  193. summary={t('iTunes credentials')}
  194. defaultExpanded={!!appStoreConnectContext?.itunesSessionValid}
  195. >
  196. {!!appStoreConnectContext?.itunesSessionValid && (
  197. <StyledAlert type="warning" icon={<IconWarning />}>
  198. {t(
  199. 'Your iTunes session has expired. To reconnect, sign in with your Apple ID and password'
  200. )}
  201. </StyledAlert>
  202. )}
  203. <ItunesCredentials
  204. api={api}
  205. orgSlug={orgSlug}
  206. projectSlug={projectSlug}
  207. data={iTunesCredentialsData}
  208. onChange={setItunesCredentialsData}
  209. onReset={() => setItunesCredentialsData(iTunesCredentialsInitialData)}
  210. isUpdating={isUpdating}
  211. />
  212. </Accordion>
  213. </StyledList>
  214. </Body>
  215. <Footer>
  216. <ButtonBar gap={1.5}>
  217. <Button onClick={closeModal}>{t('Cancel')}</Button>
  218. <StyledButton
  219. priority="primary"
  220. onClick={handleSave}
  221. disabled={isFormInvalid() || isLoading}
  222. >
  223. {t('Save')}
  224. </StyledButton>
  225. </ButtonBar>
  226. </Footer>
  227. </Fragment>
  228. );
  229. }
  230. export default withApi(AppStoreConnect);
  231. const StyledList = styled(List)`
  232. grid-gap: ${space(2)};
  233. & > li {
  234. padding-left: 0;
  235. :before {
  236. z-index: 1;
  237. left: 9px;
  238. top: ${space(1.5)};
  239. }
  240. }
  241. `;
  242. const StyledButton = styled(Button)`
  243. position: relative;
  244. `;
  245. const StyledAlert = styled(Alert)`
  246. margin: ${space(1)} 0 ${space(2)} 0;
  247. `;