sso-widget.tsx 4.6 KB

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