sso-widget.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { useCallback, useState } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import { FetchError } from '../../../../infrastructure/fetch-json'
  4. import IEEELogo from '../../../../shared/svgs/ieee-logo'
  5. import GoogleLogo from '../../../../shared/svgs/google-logo'
  6. import OrcidLogo from '../../../../shared/svgs/orcid-logo'
  7. import LinkingStatus from './status'
  8. import OLButton from '@/features/ui/components/ol/ol-button'
  9. import OLModal, {
  10. OLModalBody,
  11. OLModalFooter,
  12. OLModalHeader,
  13. OLModalTitle,
  14. } from '@/features/ui/components/ol/ol-modal'
  15. const providerLogos: { readonly [p: string]: JSX.Element } = {
  16. collabratec: <IEEELogo />,
  17. google: <GoogleLogo />,
  18. orcid: <OrcidLogo />,
  19. }
  20. type SSOLinkingWidgetProps = {
  21. providerId: string
  22. title: string
  23. description: string
  24. helpPath?: string
  25. linked?: boolean
  26. linkPath: string
  27. onUnlink: () => Promise<void>
  28. }
  29. export function SSOLinkingWidget({
  30. providerId,
  31. title,
  32. description,
  33. helpPath,
  34. linked,
  35. linkPath,
  36. onUnlink,
  37. }: SSOLinkingWidgetProps) {
  38. const { t } = useTranslation()
  39. const [showModal, setShowModal] = useState(false)
  40. const [unlinkRequestInflight, setUnlinkRequestInflight] = useState(false)
  41. const [errorMessage, setErrorMessage] = useState('')
  42. const handleUnlinkClick = useCallback(() => {
  43. setShowModal(true)
  44. setErrorMessage('')
  45. }, [])
  46. const handleUnlinkConfirmationClick = useCallback(() => {
  47. setShowModal(false)
  48. setUnlinkRequestInflight(true)
  49. onUnlink()
  50. .catch((error: FetchError) => {
  51. setErrorMessage(error.getUserFacingMessage())
  52. })
  53. .finally(() => {
  54. setUnlinkRequestInflight(false)
  55. })
  56. }, [onUnlink])
  57. const handleModalHide = useCallback(() => {
  58. setShowModal(false)
  59. }, [])
  60. return (
  61. <div className="settings-widget-container">
  62. <div>{providerLogos[providerId]}</div>
  63. <div className="description-container">
  64. <div className="title-row">
  65. <h4>{title}</h4>
  66. </div>
  67. <p className="small">
  68. {description?.replace(/<[^>]+>/g, '')}{' '}
  69. {helpPath ? (
  70. <a href={helpPath} target="_blank" rel="noreferrer">
  71. {t('learn_more')}
  72. </a>
  73. ) : null}
  74. </p>
  75. {errorMessage ? (
  76. <LinkingStatus status="error" description={errorMessage} />
  77. ) : null}
  78. </div>
  79. <div>
  80. <ActionButton
  81. unlinkRequestInflight={unlinkRequestInflight}
  82. accountIsLinked={linked}
  83. linkPath={`${linkPath}?intent=link`}
  84. onUnlinkClick={handleUnlinkClick}
  85. />
  86. </div>
  87. <UnlinkConfirmModal
  88. title={title}
  89. show={showModal}
  90. handleConfirmation={handleUnlinkConfirmationClick}
  91. handleHide={handleModalHide}
  92. />
  93. </div>
  94. )
  95. }
  96. type ActionButtonProps = {
  97. unlinkRequestInflight: boolean
  98. accountIsLinked?: boolean
  99. linkPath: string
  100. onUnlinkClick: () => void
  101. }
  102. function ActionButton({
  103. unlinkRequestInflight,
  104. accountIsLinked,
  105. linkPath,
  106. onUnlinkClick,
  107. }: ActionButtonProps) {
  108. const { t } = useTranslation()
  109. if (unlinkRequestInflight) {
  110. return (
  111. <OLButton variant="danger-ghost" disabled>
  112. {t('unlinking')}
  113. </OLButton>
  114. )
  115. } else if (accountIsLinked) {
  116. return (
  117. <OLButton variant="danger-ghost" onClick={onUnlinkClick}>
  118. {t('unlink')}
  119. </OLButton>
  120. )
  121. } else {
  122. return (
  123. <OLButton variant="secondary" href={linkPath} className="text-capitalize">
  124. {t('link')}
  125. </OLButton>
  126. )
  127. }
  128. }
  129. type UnlinkConfirmModalProps = {
  130. title: string
  131. show: boolean
  132. handleConfirmation: () => void
  133. handleHide: () => void
  134. }
  135. function UnlinkConfirmModal({
  136. title,
  137. show,
  138. handleConfirmation,
  139. handleHide,
  140. }: UnlinkConfirmModalProps) {
  141. const { t } = useTranslation()
  142. return (
  143. <OLModal show={show} onHide={handleHide}>
  144. <OLModalHeader closeButton>
  145. <OLModalTitle>
  146. {t('unlink_provider_account_title', { provider: title })}
  147. </OLModalTitle>
  148. </OLModalHeader>
  149. <OLModalBody>
  150. <p>{t('unlink_provider_account_warning', { provider: title })}</p>
  151. </OLModalBody>
  152. <OLModalFooter>
  153. <OLButton variant="secondary" onClick={handleHide}>
  154. {t('cancel')}
  155. </OLButton>
  156. <OLButton variant="danger-ghost" onClick={handleConfirmation}>
  157. {t('unlink')}
  158. </OLButton>
  159. </OLModalFooter>
  160. </OLModal>
  161. )
  162. }