sso-widget.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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 id={providerId}>{title}</h4>
  66. </div>
  67. <p className="small">
  68. {description?.replace(/<[^>]+>/g, '')}{' '}
  69. {helpPath ? (
  70. <a
  71. href={helpPath}
  72. target="_blank"
  73. rel="noreferrer"
  74. aria-label={t('learn_more_about', { integrationName: title })}
  75. >
  76. {t('learn_more')}
  77. </a>
  78. ) : null}
  79. </p>
  80. {errorMessage ? (
  81. <LinkingStatus status="error" description={errorMessage} />
  82. ) : null}
  83. </div>
  84. <div>
  85. <ActionButton
  86. titleId={providerId}
  87. unlinkRequestInflight={unlinkRequestInflight}
  88. accountIsLinked={linked}
  89. linkPath={`${linkPath}?intent=link`}
  90. onUnlinkClick={handleUnlinkClick}
  91. />
  92. </div>
  93. <UnlinkConfirmModal
  94. title={title}
  95. show={showModal}
  96. handleConfirmation={handleUnlinkConfirmationClick}
  97. handleHide={handleModalHide}
  98. />
  99. </div>
  100. )
  101. }
  102. type ActionButtonProps = {
  103. unlinkRequestInflight: boolean
  104. accountIsLinked?: boolean
  105. linkPath: string
  106. onUnlinkClick: () => void
  107. titleId: string
  108. }
  109. function ActionButton({
  110. unlinkRequestInflight,
  111. accountIsLinked,
  112. linkPath,
  113. onUnlinkClick,
  114. titleId,
  115. }: ActionButtonProps) {
  116. const { t } = useTranslation()
  117. const linkTextId = `${titleId}-link`
  118. if (unlinkRequestInflight) {
  119. return (
  120. <OLButton variant="danger-ghost" disabled>
  121. {t('unlinking')}
  122. </OLButton>
  123. )
  124. } else if (accountIsLinked) {
  125. return (
  126. <OLButton
  127. variant="danger-ghost"
  128. onClick={onUnlinkClick}
  129. aria-labelledby={`${linkTextId} ${titleId}`}
  130. id={linkTextId}
  131. >
  132. {t('unlink')}
  133. </OLButton>
  134. )
  135. } else {
  136. return (
  137. <OLButton
  138. variant="secondary"
  139. href={linkPath}
  140. className="text-capitalize"
  141. aria-labelledby={`${linkTextId} ${titleId}`}
  142. id={linkTextId}
  143. >
  144. {t('link')}
  145. </OLButton>
  146. )
  147. }
  148. }
  149. type UnlinkConfirmModalProps = {
  150. title: string
  151. show: boolean
  152. handleConfirmation: () => void
  153. handleHide: () => void
  154. }
  155. function UnlinkConfirmModal({
  156. title,
  157. show,
  158. handleConfirmation,
  159. handleHide,
  160. }: UnlinkConfirmModalProps) {
  161. const { t } = useTranslation()
  162. return (
  163. <OLModal show={show} onHide={handleHide}>
  164. <OLModalHeader closeButton>
  165. <OLModalTitle>
  166. {t('unlink_provider_account_title', { provider: title })}
  167. </OLModalTitle>
  168. </OLModalHeader>
  169. <OLModalBody>
  170. <p>{t('unlink_provider_account_warning', { provider: title })}</p>
  171. </OLModalBody>
  172. <OLModalFooter>
  173. <OLButton variant="secondary" onClick={handleHide}>
  174. {t('cancel')}
  175. </OLButton>
  176. <OLButton variant="danger-ghost" onClick={handleConfirmation}>
  177. {t('unlink')}
  178. </OLButton>
  179. </OLModalFooter>
  180. </OLModal>
  181. )
  182. }