token-access-root.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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 importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  15. import MaterialIcon from '@/shared/components/material-icon'
  16. import { useFeatureFlag } from '@/shared/context/split-test-context'
  17. type Mode = 'access-attempt' | 'v1Import' | 'requireAccept'
  18. const [v1ImportDataScreenModule] = importOverleafModules(
  19. 'v1ImportDataScreen'
  20. ) as {
  21. import: { default: ElementType }
  22. }[]
  23. const V1ImportDataScreen = v1ImportDataScreenModule?.import.default
  24. export type V1ImportData = {
  25. name?: string
  26. status: string
  27. projectId: string
  28. }
  29. function TokenAccessRoot() {
  30. const [mode, setMode] = useState<Mode>('access-attempt')
  31. const [inflight, setInflight] = useState(false)
  32. const [accessError, setAccessError] = useState<string | boolean>(false)
  33. const [v1ImportData, setV1ImportData] = useState<V1ImportData>()
  34. const [requireAcceptData, setRequireAcceptData] =
  35. useState<RequireAcceptData>()
  36. const [loadingScreenBrandHeight, setLoadingScreenBrandHeight] =
  37. useState('0px')
  38. const location = useLocation()
  39. const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
  40. const sendPostRequest = useCallback(
  41. (confirmedByUser = false) => {
  42. setInflight(true)
  43. postJSON(getMeta('ol-postUrl'), {
  44. body: {
  45. confirmedByUser,
  46. tokenHashPrefix: document.location.hash,
  47. },
  48. })
  49. .then(async data => {
  50. setAccessError(false)
  51. if (data.redirect) {
  52. location.replace(data.redirect)
  53. } else if (data.v1Import) {
  54. setMode('v1Import')
  55. setV1ImportData(data.v1Import)
  56. } else if (data.requireAccept) {
  57. setMode('requireAccept')
  58. setRequireAcceptData(data.requireAccept)
  59. } else {
  60. debugConsole.warn(
  61. 'invalid data from server in success response',
  62. data
  63. )
  64. setAccessError(true)
  65. }
  66. })
  67. .catch(error => {
  68. debugConsole.warn('error response from server', error)
  69. setAccessError(error.response?.status === 404 ? 'not_found' : 'error')
  70. })
  71. .finally(() => {
  72. setInflight(false)
  73. })
  74. },
  75. [location]
  76. )
  77. const postedRef = useRef(false)
  78. useEffect(() => {
  79. if (!postedRef.current) {
  80. postedRef.current = true
  81. sendPostRequest()
  82. setTimeout(() => {
  83. setLoadingScreenBrandHeight('20%')
  84. }, 500)
  85. }
  86. }, [sendPostRequest])
  87. const { isReady } = useWaitForI18n()
  88. if (!isReady) {
  89. return null
  90. }
  91. // We don't want the full-size div and back link(?) on
  92. // the new page, but we do this so the original page
  93. // doesn't change.
  94. // TODO: clean up the DOM in the main return
  95. if (mode === 'requireAccept' && requireAcceptData) {
  96. return (
  97. <RequireAcceptScreen
  98. requireAcceptData={requireAcceptData}
  99. sendPostRequest={sendPostRequest}
  100. />
  101. )
  102. }
  103. return (
  104. <div className="token-access-container">
  105. {!isSharingUpdatesEnabled && (
  106. <div className="token-access-action-header">
  107. <a href="/project" className="token-access-home-link">
  108. <MaterialIcon
  109. type="arrow_left_alt"
  110. style={{ fontSize: 'inherit' }}
  111. />
  112. </a>
  113. </div>
  114. )}
  115. <div className="token-access-content">
  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. </div>
  127. </div>
  128. )
  129. }
  130. export default withErrorBoundary(TokenAccessRoot, () => (
  131. <GenericErrorBoundaryFallback />
  132. ))