token-access-root.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
  2. import withErrorBoundary from '@/infrastructure/error-boundary'
  3. import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
  4. import { ElementType, useCallback, useEffect, useRef, useState } from 'react'
  5. import getMeta from '@/utils/meta'
  6. import { postJSON } from '@/infrastructure/fetch-json'
  7. import { debugConsole } from '@/utils/debugging'
  8. import { useLocation } from '@/shared/hooks/use-location'
  9. import { AccessAttemptScreen } from '@/features/token-access/components/access-attempt-screen'
  10. import {
  11. RequireAcceptData,
  12. RequireAcceptScreen,
  13. } from '@/features/token-access/components/require-accept-screen'
  14. import Icon from '@/shared/components/icon'
  15. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  16. type Mode = 'access-attempt' | 'v1Import' | 'requireAccept'
  17. const [v1ImportDataScreenModule] = importOverleafModules(
  18. 'v1ImportDataScreen'
  19. ) as {
  20. import: { default: ElementType }
  21. }[]
  22. const V1ImportDataScreen = v1ImportDataScreenModule?.import.default
  23. export type V1ImportData = {
  24. name?: string
  25. status: string
  26. projectId: string
  27. }
  28. function TokenAccessRoot() {
  29. const [mode, setMode] = useState<Mode>('access-attempt')
  30. const [inflight, setInflight] = useState(false)
  31. const [accessError, setAccessError] = useState<string | boolean>(false)
  32. const [v1ImportData, setV1ImportData] = useState<V1ImportData>()
  33. const [requireAcceptData, setRequireAcceptData] =
  34. useState<RequireAcceptData>()
  35. const [loadingScreenBrandHeight, setLoadingScreenBrandHeight] =
  36. useState('0px')
  37. const location = useLocation()
  38. const sendPostRequest = useCallback(
  39. (confirmedByUser = false) => {
  40. setInflight(true)
  41. postJSON(getMeta('ol-postUrl'), {
  42. body: {
  43. confirmedByUser,
  44. tokenHashPrefix: document.location.hash,
  45. },
  46. })
  47. .then(async data => {
  48. setAccessError(false)
  49. if (data.redirect) {
  50. location.replace(data.redirect)
  51. } else if (data.v1Import) {
  52. setMode('v1Import')
  53. setV1ImportData(data.v1Import)
  54. } else if (data.requireAccept) {
  55. setMode('requireAccept')
  56. setRequireAcceptData(data.requireAccept)
  57. } else {
  58. debugConsole.warn(
  59. 'invalid data from server in success response',
  60. data
  61. )
  62. setAccessError(true)
  63. }
  64. })
  65. .catch(error => {
  66. debugConsole.warn('error response from server', error)
  67. setAccessError(error.response?.status === 404 ? 'not_found' : 'error')
  68. })
  69. .finally(() => {
  70. setInflight(false)
  71. })
  72. },
  73. [location]
  74. )
  75. const postedRef = useRef(false)
  76. useEffect(() => {
  77. if (!postedRef.current) {
  78. postedRef.current = true
  79. sendPostRequest()
  80. setTimeout(() => {
  81. setLoadingScreenBrandHeight('20%')
  82. }, 500)
  83. }
  84. }, [sendPostRequest])
  85. const { isReady } = useWaitForI18n()
  86. if (!isReady) {
  87. return null
  88. }
  89. // We don't want the full-size div and back link(?) on
  90. // the new page, but we do this so the original page
  91. // doesn't change. When tearing down we can clean up
  92. // the DOM in the main return
  93. if (
  94. mode === 'requireAccept' &&
  95. requireAcceptData &&
  96. requireAcceptData.linkSharingChanges
  97. ) {
  98. return (
  99. <RequireAcceptScreen
  100. requireAcceptData={requireAcceptData}
  101. sendPostRequest={sendPostRequest}
  102. />
  103. )
  104. }
  105. return (
  106. <div className="full-size">
  107. <div>
  108. <a
  109. href="/project"
  110. // TODO: class name
  111. style={{ fontSize: '2rem', marginLeft: '1rem', color: '#ddd' }}
  112. >
  113. <Icon type="arrow-left" />
  114. </a>
  115. </div>
  116. {mode === 'access-attempt' && (
  117. <AccessAttemptScreen
  118. accessError={accessError}
  119. inflight={inflight}
  120. loadingScreenBrandHeight={loadingScreenBrandHeight}
  121. />
  122. )}
  123. {V1ImportDataScreen && mode === 'v1Import' && v1ImportData && (
  124. <V1ImportDataScreen v1ImportData={v1ImportData} />
  125. )}
  126. {mode === 'requireAccept' && requireAcceptData && (
  127. <RequireAcceptScreen
  128. requireAcceptData={requireAcceptData}
  129. sendPostRequest={sendPostRequest}
  130. />
  131. )}
  132. </div>
  133. )
  134. }
  135. export default withErrorBoundary(TokenAccessRoot, GenericErrorBoundaryFallback)