sso-widget.tsx 4.8 KB

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