sso-widget.tsx 4.7 KB

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