sso-widget.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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 { bsVersion } from '@/features/utils/bootstrap-5'
  10. import OLModal, {
  11. OLModalBody,
  12. OLModalFooter,
  13. OLModalHeader,
  14. OLModalTitle,
  15. } from '@/features/ui/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>{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')}
  73. </a>
  74. ) : null}
  75. </p>
  76. {errorMessage ? (
  77. <LinkingStatus status="error" description={errorMessage} />
  78. ) : null}
  79. </div>
  80. <div>
  81. <ActionButton
  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. }
  103. function ActionButton({
  104. unlinkRequestInflight,
  105. accountIsLinked,
  106. linkPath,
  107. onUnlinkClick,
  108. }: ActionButtonProps) {
  109. const { t } = useTranslation()
  110. if (unlinkRequestInflight) {
  111. return (
  112. <OLButton
  113. variant="danger-ghost"
  114. disabled
  115. bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
  116. >
  117. {t('unlinking')}
  118. </OLButton>
  119. )
  120. } else if (accountIsLinked) {
  121. return (
  122. <OLButton
  123. variant="danger-ghost"
  124. onClick={onUnlinkClick}
  125. bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
  126. >
  127. {t('unlink')}
  128. </OLButton>
  129. )
  130. } else {
  131. return (
  132. <OLButton
  133. variant="secondary"
  134. href={linkPath}
  135. bs3Props={{ bsStyle: null }}
  136. className={bsVersion({
  137. bs3: 'btn btn-secondary-info btn-secondary text-capitalize',
  138. bs5: 'text-capitalize',
  139. })}
  140. >
  141. {t('link')}
  142. </OLButton>
  143. )
  144. }
  145. }
  146. type UnlinkConfirmModalProps = {
  147. title: string
  148. show: boolean
  149. handleConfirmation: () => void
  150. handleHide: () => void
  151. }
  152. function UnlinkConfirmModal({
  153. title,
  154. show,
  155. handleConfirmation,
  156. handleHide,
  157. }: UnlinkConfirmModalProps) {
  158. const { t } = useTranslation()
  159. return (
  160. <OLModal show={show} onHide={handleHide}>
  161. <OLModalHeader closeButton>
  162. <OLModalTitle>
  163. {t('unlink_provider_account_title', { provider: title })}
  164. </OLModalTitle>
  165. </OLModalHeader>
  166. <OLModalBody>
  167. <p>{t('unlink_provider_account_warning', { provider: title })}</p>
  168. </OLModalBody>
  169. <OLModalFooter>
  170. <OLButton
  171. variant="secondary"
  172. onClick={handleHide}
  173. bs3Props={{
  174. bsStyle: null,
  175. className: 'btn-secondary-info btn-secondary',
  176. }}
  177. >
  178. {t('cancel')}
  179. </OLButton>
  180. <OLButton
  181. variant="danger-ghost"
  182. onClick={handleConfirmation}
  183. bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
  184. >
  185. {t('unlink')}
  186. </OLButton>
  187. </OLModalFooter>
  188. </OLModal>
  189. )
  190. }