policyRow.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import {Fragment} from 'react';
  2. import type {Theme} from '@emotion/react';
  3. import {css, useTheme} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import moment from 'moment-timezone';
  6. import {openModal} from 'sentry/actionCreators/modal';
  7. import {Button, LinkButton} from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import {t, tct} from 'sentry/locale';
  10. import ConfigStore from 'sentry/stores/configStore';
  11. import {space} from 'sentry/styles/space';
  12. import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
  13. import {safeURL} from 'sentry/utils/url/safeURL';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import type {Policy, Subscription} from 'getsentry/types';
  16. import {PanelItemPolicy} from 'getsentry/views/legalAndCompliance/styles';
  17. type PolicyRowProps = {
  18. onAccept: (policy: Policy) => void;
  19. policies: Record<string, Policy>;
  20. policy: Policy;
  21. subscription: Subscription;
  22. showConsentText?: boolean;
  23. showUpdated?: boolean;
  24. };
  25. // TODO(dcramer): we dont yet support multiple parent policies if a policy in the
  26. // chain does not require signature (and instead would just have you page through it)
  27. export function PolicyRow({
  28. policy,
  29. policies,
  30. showUpdated,
  31. showConsentText = true,
  32. onAccept,
  33. subscription,
  34. }: PolicyRowProps) {
  35. const theme = useTheme();
  36. const organization = useOrganization();
  37. const parentPolicy = policy.parent ? policies[policy.parent] : null;
  38. const curPolicy = parentPolicy && !parentPolicy.consent ? parentPolicy : policy;
  39. const user = ConfigStore.get('user');
  40. const companyName = subscription?.companyName ?? organization.name;
  41. const activeSuperUser = isActiveSuperuser();
  42. const hasBillingAccess = organization.access.includes('org:billing');
  43. const policyUrl = policy.url ? safeURL(policy.url) : null;
  44. // userCurrentVersion filters version select dropdown to only the current version + latest version
  45. if (policyUrl && policy.consent) {
  46. policyUrl.searchParams.set('userCurrentVersion', policy.consent.acceptedVersion);
  47. }
  48. const showPolicy = (e: React.MouseEvent) => {
  49. let dialog: Window | null = null;
  50. e.preventDefault();
  51. const name = 'sentryPolicy';
  52. const width = 600;
  53. const height = 600;
  54. const url = policy.url;
  55. // this attempts to center the dialog
  56. const innerWidth = window.innerWidth
  57. ? window.innerWidth
  58. : document.documentElement.clientWidth
  59. ? document.documentElement.clientWidth
  60. : screen.width;
  61. const innerHeight = window.innerHeight
  62. ? window.innerHeight
  63. : document.documentElement.clientHeight
  64. ? document.documentElement.clientHeight
  65. : screen.height;
  66. const left = innerWidth / 2 - width / 2 + window.screenLeft;
  67. const top = innerHeight / 2 - height / 2 + window.screenTop;
  68. dialog = url
  69. ? window.open(
  70. url,
  71. name,
  72. `scrollbars=yes, width=${width}, height=${height}, top=${top}, left=${left}`
  73. )
  74. : null;
  75. // @ts-expect-error TS(2774): This condition will always return true since this ... Remove this comment to see the full error message
  76. if (window.focus) {
  77. dialog?.focus();
  78. }
  79. };
  80. const showModal = () => {
  81. openModal(
  82. ({Header, Footer, Body, closeModal}) => (
  83. <Fragment>
  84. <Header>
  85. {curPolicy.slug !== policy.slug ? (
  86. <div style={{textAlign: 'center'}}>
  87. {tct("You must first agree to Sentry's [policy]", {
  88. policy: <a onClick={showPolicy}>{curPolicy.name}</a>,
  89. })}
  90. </div>
  91. ) : (
  92. <PolicyHeader>
  93. <h5>{curPolicy.name}</h5>
  94. <Button size="sm" onClick={showPolicy}>
  95. {t('Download')}
  96. </Button>
  97. </PolicyHeader>
  98. )}
  99. </Header>
  100. <Body>
  101. <PolicyFrame
  102. src={policyUrl ? policyUrl.toString() : undefined}
  103. data-test-id="policy-iframe"
  104. />
  105. {curPolicy.hasSignature && (
  106. <div style={{fontSize: '0.9em'}}>
  107. <p style={{marginBottom: 10}}>You represent and warrant that:</p>
  108. <ol style={{marginBottom: 10}}>
  109. <li>
  110. you have full legal authority to agree to these terms presented above
  111. on behalf of <strong>{companyName}</strong>;
  112. </li>
  113. <li>you have read and understand these terms; and</li>
  114. <li>
  115. you agree, on behalf of <strong>{companyName}</strong>, to these
  116. terms.
  117. </li>
  118. </ol>
  119. <p>
  120. If you do not have the authority to bind <strong>{companyName}</strong>,
  121. or do not agree to these terms, do not click the "I Accept" button
  122. below.
  123. </p>
  124. </div>
  125. )}
  126. </Body>
  127. <Footer>
  128. {curPolicy.hasSignature ? (
  129. <PolicyActions>
  130. <small>
  131. {tct('You are agreeing as [email]', {
  132. email: <strong>{user.email}</strong>,
  133. })}
  134. </small>
  135. <ButtonBar gap={1}>
  136. <Button size="sm" onClick={closeModal}>
  137. {t('Cancel')}
  138. </Button>
  139. <Button
  140. size="sm"
  141. priority="primary"
  142. onClick={() => {
  143. onAccept(curPolicy);
  144. closeModal();
  145. }}
  146. >
  147. {t('I Accept')}
  148. </Button>
  149. </ButtonBar>
  150. </PolicyActions>
  151. ) : (
  152. <Button size="sm" onClick={closeModal}>
  153. {t('Close')}
  154. </Button>
  155. )}
  156. </Footer>
  157. </Fragment>
  158. ),
  159. {modalCss: modalCss(theme)}
  160. );
  161. };
  162. const getPolicySubstatus = () => {
  163. const {consent, updatedAt, version} = policy;
  164. if (consent && showConsentText) {
  165. let consentText = `Version ${consent.acceptedVersion} signed ${moment(
  166. consent.createdAt
  167. ).format('ll')}`;
  168. if (version && consent.acceptedVersion < version) {
  169. consentText = `${consentText}. New version available`;
  170. }
  171. return consentText;
  172. }
  173. if (showUpdated) {
  174. return `Updated on ${moment(updatedAt).format('ll')}`;
  175. }
  176. return '';
  177. };
  178. return (
  179. <PanelItemPolicy>
  180. <div>
  181. <PolicyTitle style={{marginBottom: showUpdated ? space(0.5) : 0}}>
  182. {policy.slug === 'terms' ? 'Terms of Service' : policy.name}
  183. </PolicyTitle>
  184. <PolicySubtext>{getPolicySubstatus()}</PolicySubtext>
  185. </div>
  186. <div>
  187. {policy.url &&
  188. policyUrl &&
  189. (policy.consent?.acceptedVersion === policy.version ? (
  190. <LinkButton size="sm" external href={policyUrl.toString()}>
  191. {t('Review')}
  192. </LinkButton>
  193. ) : policy.hasSignature &&
  194. policy.slug !== 'privacy' &&
  195. policy.slug !== 'terms' ? (
  196. <Button
  197. size="sm"
  198. priority="primary"
  199. onClick={showModal}
  200. disabled={activeSuperUser || !hasBillingAccess}
  201. title={
  202. activeSuperUser
  203. ? t("Superusers can't consent to policies")
  204. : !hasBillingAccess
  205. ? t(
  206. "You don't have access to manage billing and subscription details."
  207. )
  208. : undefined
  209. }
  210. >
  211. {t('Review and Accept')}
  212. </Button>
  213. ) : (
  214. <LinkButton size="sm" external href={policy.url}>
  215. {t('Review')}
  216. </LinkButton>
  217. ))}
  218. </div>
  219. </PanelItemPolicy>
  220. );
  221. }
  222. const PolicyFrame = styled('iframe')`
  223. height: 300px;
  224. width: 100%;
  225. border: 1px solid ${p => p.theme.innerBorder};
  226. border-radius: 3px;
  227. margin-bottom: ${space(1)};
  228. `;
  229. const PolicySubtext = styled('div')`
  230. font-size: ${p => p.theme.fontSizeSmall};
  231. color: ${p => p.theme.subText};
  232. `;
  233. const PolicyTitle = styled('h6')`
  234. @media (max-width: ${p => p.theme.breakpoints.small}) {
  235. font-size: ${p => p.theme.fontSizeLarge};
  236. }
  237. `;
  238. const PolicyHeader = styled('div')`
  239. display: flex;
  240. align-items: center;
  241. justify-content: space-between;
  242. `;
  243. const PolicyActions = styled('div')`
  244. flex-grow: 1;
  245. display: flex;
  246. align-items: center;
  247. justify-content: space-between;
  248. `;
  249. const modalCss = (theme: Theme) => css`
  250. @media (min-width: ${theme.breakpoints.small}) {
  251. width: 80%;
  252. max-width: 1200px;
  253. }
  254. `;