add-email.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import { useState, useEffect } from 'react'
  2. import { useTranslation, Trans } from 'react-i18next'
  3. import Cell from './cell'
  4. import Layout from './add-email/layout'
  5. import Input, { DomainInfo } from './add-email/input'
  6. import AddAnotherEmailBtn from './add-email/add-another-email-btn'
  7. import InstitutionFields from './add-email/institution-fields'
  8. import SsoLinkingInfo from './add-email/sso-linking-info'
  9. import AddNewEmailBtn from './add-email/add-new-email-btn'
  10. import useAsync from '../../../../shared/hooks/use-async'
  11. import { useUserEmailsContext } from '../../context/user-email-context'
  12. import { ssoAvailableForDomain } from '../../utils/sso'
  13. import { postJSON } from '../../../../infrastructure/fetch-json'
  14. import { University } from '../../../../../../types/university'
  15. import { CountryCode } from '../../data/countries-list'
  16. import { isValidEmail } from '../../../../shared/utils/email'
  17. import getMeta from '../../../../utils/meta'
  18. import { ReCaptcha2 } from '../../../../shared/components/recaptcha-2'
  19. import { useRecaptcha } from '../../../../shared/hooks/use-recaptcha'
  20. import OLCol from '@/features/ui/components/ol/ol-col'
  21. import { ConfirmEmailForm } from '@/features/settings/components/emails/confirm-email-form'
  22. import RecaptchaConditions from '@/shared/components/recaptcha-conditions'
  23. import SsoLinkingInfoGroup from './add-email/sso-linking-info-group'
  24. import Notification from '@/shared/components/notification'
  25. function AddEmail() {
  26. const { t } = useTranslation()
  27. const [isFormVisible, setIsFormVisible] = useState(
  28. () => window.location.hash === '#add-email'
  29. )
  30. const [newEmail, setNewEmail] = useState('')
  31. const [confirmationStep, setConfirmationStep] = useState(false)
  32. const [newEmailMatchedDomain, setNewEmailMatchedDomain] =
  33. useState<DomainInfo | null>(null)
  34. const [countryCode, setCountryCode] = useState<CountryCode | null>(null)
  35. const [universities, setUniversities] = useState<
  36. Partial<Record<CountryCode, University[]>>
  37. >({})
  38. const [universityName, setUniversityName] = useState('')
  39. const [role, setRole] = useState('')
  40. const [department, setDepartment] = useState('')
  41. const { isLoading, isError, error, runAsync } = useAsync()
  42. const {
  43. state,
  44. setLoading: setUserEmailsContextLoading,
  45. getEmails,
  46. } = useUserEmailsContext()
  47. const emailAddressLimit = getMeta('ol-emailAddressLimit') || 10
  48. const { ref: recaptchaRef, getReCaptchaToken } = useRecaptcha()
  49. useEffect(() => {
  50. setUserEmailsContextLoading(isLoading)
  51. }, [setUserEmailsContextLoading, isLoading])
  52. const handleShowAddEmailForm = () => {
  53. setIsFormVisible(true)
  54. }
  55. const handleEmailChange = (value: string, domain?: DomainInfo) => {
  56. setNewEmail(value)
  57. setNewEmailMatchedDomain(domain || null)
  58. }
  59. const getSelectedKnownUniversityId = (): number | undefined => {
  60. if (countryCode) {
  61. return universities[countryCode]?.find(
  62. ({ name }) => name === universityName
  63. )?.id
  64. }
  65. return newEmailMatchedDomain?.university.id
  66. }
  67. const handleAddNewEmail = () => {
  68. const selectedKnownUniversityId = getSelectedKnownUniversityId()
  69. const knownUniversityData = selectedKnownUniversityId && {
  70. university: {
  71. id: selectedKnownUniversityId,
  72. },
  73. role,
  74. department,
  75. }
  76. const unknownUniversityData = universityName &&
  77. !selectedKnownUniversityId && {
  78. university: {
  79. name: universityName,
  80. country_code: countryCode,
  81. },
  82. role,
  83. department,
  84. }
  85. runAsync(
  86. (async () => {
  87. const token = await getReCaptchaToken()
  88. await postJSON('/user/emails/secondary', {
  89. body: {
  90. email: newEmail,
  91. ...knownUniversityData,
  92. ...unknownUniversityData,
  93. 'g-recaptcha-response': token,
  94. },
  95. })
  96. })()
  97. )
  98. .then(() => {
  99. setConfirmationStep(true)
  100. })
  101. .catch(() => {})
  102. }
  103. if (confirmationStep) {
  104. return (
  105. <ConfirmEmailForm
  106. confirmationEndpoint="/user/emails/confirm-secondary"
  107. resendEndpoint="/user/emails/resend-secondary-confirmation"
  108. flow="secondary"
  109. email={newEmail}
  110. onSuccessfulConfirmation={getEmails}
  111. interstitial={false}
  112. onCancel={() => {
  113. setConfirmationStep(false)
  114. setIsFormVisible(false)
  115. }}
  116. />
  117. )
  118. }
  119. if (!isFormVisible) {
  120. return (
  121. <Layout isError={isError} error={error}>
  122. <OLCol lg={12}>
  123. <Cell>
  124. {state.data.emailCount >= emailAddressLimit ? (
  125. <span className="small">
  126. <Trans
  127. i18nKey="email_limit_reached"
  128. values={{
  129. emailAddressLimit,
  130. }}
  131. shouldUnescape
  132. tOptions={{ interpolation: { escapeValue: true } }}
  133. components={[<strong />]} // eslint-disable-line react/jsx-key
  134. />
  135. </span>
  136. ) : (
  137. <AddAnotherEmailBtn onClick={handleShowAddEmailForm} />
  138. )}
  139. </Cell>
  140. </OLCol>
  141. </Layout>
  142. )
  143. }
  144. const InputComponent = (
  145. <>
  146. <label htmlFor="affiliations-email" className="visually-hidden">
  147. {t('email')}
  148. </label>
  149. <Input
  150. onChange={handleEmailChange}
  151. handleAddNewEmail={handleAddNewEmail}
  152. />
  153. </>
  154. )
  155. const recaptchaConditions = (
  156. <OLCol>
  157. <Cell>
  158. <div className="affiliations-table-cell-tabbed">
  159. <RecaptchaConditions />
  160. </div>
  161. </Cell>
  162. </OLCol>
  163. )
  164. if (!isValidEmail(newEmail)) {
  165. return (
  166. <form>
  167. <Layout isError={isError} error={error}>
  168. <ReCaptcha2 page="addEmail" recaptchaRef={recaptchaRef} />
  169. <OLCol lg={8}>
  170. <Cell>
  171. {InputComponent}
  172. <div className="affiliations-table-cell-tabbed">
  173. <div>{t('start_by_adding_your_email')}</div>
  174. </div>
  175. </Cell>
  176. </OLCol>
  177. <OLCol lg={4}>
  178. <Cell className="text-lg-end">
  179. <AddNewEmailBtn email={newEmail} disabled />
  180. </Cell>
  181. </OLCol>
  182. {recaptchaConditions}
  183. </Layout>
  184. </form>
  185. )
  186. }
  187. const isSsoAvailableForDomain =
  188. newEmailMatchedDomain && ssoAvailableForDomain(newEmailMatchedDomain)
  189. return (
  190. <form>
  191. <Layout isError={isError} error={error}>
  192. <ReCaptcha2 page="addEmail" recaptchaRef={recaptchaRef} />
  193. <OLCol lg={8}>
  194. <Cell>
  195. {InputComponent}
  196. {!isSsoAvailableForDomain ? (
  197. <div className="affiliations-table-cell-tabbed">
  198. <InstitutionFields
  199. countryCode={countryCode}
  200. setCountryCode={setCountryCode}
  201. universities={universities}
  202. setUniversities={setUniversities}
  203. universityName={universityName}
  204. setUniversityName={setUniversityName}
  205. role={role}
  206. setRole={setRole}
  207. department={department}
  208. setDepartment={setDepartment}
  209. newEmailMatchedDomain={newEmailMatchedDomain}
  210. />
  211. </div>
  212. ) : null}
  213. </Cell>
  214. </OLCol>
  215. {!isSsoAvailableForDomain ? (
  216. <OLCol lg={4}>
  217. <Cell className="text-lg-end">
  218. <AddNewEmailBtn
  219. email={newEmail}
  220. disabled={state.isLoading}
  221. isLoading={isLoading}
  222. onClick={handleAddNewEmail}
  223. />
  224. </Cell>
  225. </OLCol>
  226. ) : (
  227. <OLCol lg={12}>
  228. <Cell>
  229. <div className="affiliations-table-cell-tabbed">
  230. <AddEmailViaSSO
  231. email={newEmail}
  232. domainInfo={newEmailMatchedDomain}
  233. />
  234. </div>
  235. </Cell>
  236. </OLCol>
  237. )}
  238. {recaptchaConditions}
  239. </Layout>
  240. </form>
  241. )
  242. }
  243. function AddEmailViaSSO({
  244. email,
  245. domainInfo,
  246. }: {
  247. email: string
  248. domainInfo: DomainInfo
  249. }) {
  250. if (domainInfo.university.ssoEnabled) {
  251. // SSO for Commons institution
  252. return <SsoLinkingInfo email={email} domainInfo={domainInfo} />
  253. } else if (
  254. domainInfo.group?.domainCaptureEnabled &&
  255. domainInfo.group?.managedUsersEnabled
  256. ) {
  257. return (
  258. <Notification
  259. type="error"
  260. ariaLive="polite"
  261. content={
  262. <>
  263. Your company email address has been registered under a verified
  264. domain, and cannot be added as a secondary email. Please create a
  265. new <strong>Overleaf</strong> account linked to this email address.
  266. </>
  267. }
  268. />
  269. )
  270. } else if (
  271. domainInfo.group?.domainCaptureEnabled &&
  272. domainInfo.group?.ssoConfig?.enabled
  273. ) {
  274. return <SsoLinkingInfoGroup domainInfo={domainInfo} />
  275. }
  276. }
  277. export default AddEmail