AccountIdentities.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import dayjs from 'dayjs'
  2. import { Edit, Unlink } from 'lucide-react'
  3. import Image from 'next/image'
  4. import Link from 'next/link'
  5. import { useRouter } from 'next/router'
  6. import { useEffect, useState } from 'react'
  7. import { toast } from 'sonner'
  8. import {
  9. Badge,
  10. Button,
  11. Card,
  12. CardContent,
  13. cn,
  14. Dialog,
  15. DialogContent,
  16. DialogHeader,
  17. DialogTitle,
  18. Tooltip,
  19. TooltipContent,
  20. TooltipTrigger,
  21. } from 'ui'
  22. import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
  23. import {
  24. PageSection,
  25. PageSectionContent,
  26. PageSectionDescription,
  27. PageSectionMeta,
  28. PageSectionSummary,
  29. PageSectionTitle,
  30. } from 'ui-patterns/PageSection'
  31. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  32. import {
  33. ChangeEmailAddressForm,
  34. GitHubChangeEmailAddress,
  35. SSOChangeEmailAddress,
  36. } from './ChangeEmailAddress'
  37. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  38. import { useProfileIdentitiesQuery } from '@/data/profile/profile-identities-query'
  39. import { useUnlinkIdentityMutation } from '@/data/profile/profile-unlink-identity-mutation'
  40. import { BASE_PATH } from '@/lib/constants'
  41. const getProviderName = (provider: string) =>
  42. provider === 'github'
  43. ? 'GitHub'
  44. : provider.startsWith('sso')
  45. ? 'SSO'
  46. : provider.replaceAll('_', ' ')
  47. export const AccountIdentities = () => {
  48. const router = useRouter()
  49. const { data, isPending: isLoading, isSuccess } = useProfileIdentitiesQuery()
  50. const identities = data?.identities ?? []
  51. const isChangeExpired = data?.email_change_sent_at
  52. ? dayjs().utc().diff(dayjs(data?.email_change_sent_at).utc(), 'minute') > 10
  53. : false
  54. const [selectedProviderUnlink, setSelectedProviderUnlink] = useState<string>()
  55. const [selectedProviderUpdateEmail, setSelectedProviderUpdateEmail] = useState<string>()
  56. const { mutate: unlinkIdentity, isPending: isUnlinking } = useUnlinkIdentityMutation({
  57. onSuccess: () => {
  58. toast.success(
  59. `Successfully unlinked ${getProviderName(selectedProviderUnlink ?? '')} identity!`
  60. )
  61. setSelectedProviderUnlink(undefined)
  62. },
  63. })
  64. const [, message] = router.asPath.split('#message=')
  65. const onConfirmUnlinkIdentity = async () => {
  66. const identity = identities.find((i) => i.provider === selectedProviderUnlink)
  67. if (identity) unlinkIdentity(identity)
  68. }
  69. useEffect(() => {
  70. if (message) toast.success(message.replaceAll('+', ' '))
  71. }, [message])
  72. return (
  73. <PageSection>
  74. <PageSectionMeta>
  75. <PageSectionSummary>
  76. <PageSectionTitle>Account identities</PageSectionTitle>
  77. <PageSectionDescription>
  78. Manage the providers linked to your Briven account and update their details.
  79. </PageSectionDescription>
  80. </PageSectionSummary>
  81. </PageSectionMeta>
  82. <PageSectionContent>
  83. <Card>
  84. {isLoading && (
  85. <CardContent>
  86. <ShimmeringLoader />
  87. </CardContent>
  88. )}
  89. {isSuccess && (
  90. <div className="divide-y">
  91. {identities.map((identity) => {
  92. const { identity_id, provider } = identity
  93. const username = identity.identity_data?.user_name
  94. const providerName = getProviderName(provider)
  95. const iconKey =
  96. provider === 'github'
  97. ? 'github-icon'
  98. : provider === 'email'
  99. ? 'email-icon2'
  100. : 'saml-icon'
  101. return (
  102. <CardContent key={identity_id} className="flex justify-between items-center py-4">
  103. <div className="flex gap-x-4">
  104. <Image
  105. className={cn(iconKey === 'github-icon' ? 'dark:invert' : '')}
  106. src={`${BASE_PATH}/img/icons/${iconKey}.svg`}
  107. width={30}
  108. height={30}
  109. alt={`${identity.provider} icon`}
  110. />
  111. <div>
  112. <div className="flex items-center gap-x-2">
  113. <p className="text-sm capitalize">{providerName}</p>
  114. {provider === 'email' && data.new_email && !isChangeExpired && (
  115. <Tooltip>
  116. <TooltipTrigger className="flex items-center">
  117. <Badge variant="default">Pending change</Badge>
  118. </TooltipTrigger>
  119. <TooltipContent>Changing to {data.new_email}</TooltipContent>
  120. </Tooltip>
  121. )}
  122. </div>
  123. <p className="text-sm text-foreground-lighter">
  124. {!!username ? <span>{username} · </span> : null}
  125. {identity.email}
  126. </p>
  127. </div>
  128. </div>
  129. <div className="flex items-center gap-x-1">
  130. {provider === 'email' && (
  131. <Button asChild type="default">
  132. <Link href="/reset-password?type=change">Change password</Link>
  133. </Button>
  134. )}
  135. <ButtonTooltip
  136. type="text"
  137. icon={<Edit />}
  138. className="w-7"
  139. onClick={() => setSelectedProviderUpdateEmail(provider)}
  140. tooltip={{ content: { side: 'bottom', text: 'Update email address' } }}
  141. />
  142. {identities.length > 1 && (
  143. <ButtonTooltip
  144. type="text"
  145. icon={<Unlink />}
  146. className="w-7"
  147. onClick={() => setSelectedProviderUnlink(provider)}
  148. tooltip={{ content: { side: 'bottom', text: 'Unlink identity' } }}
  149. />
  150. )}
  151. </div>
  152. </CardContent>
  153. )
  154. })}
  155. </div>
  156. )}
  157. </Card>
  158. <Dialog
  159. open={!!selectedProviderUpdateEmail}
  160. onOpenChange={(open: boolean) => {
  161. if (!open) setSelectedProviderUpdateEmail(undefined)
  162. }}
  163. >
  164. <DialogContent>
  165. <DialogHeader className="border-b">
  166. <DialogTitle>
  167. {selectedProviderUpdateEmail !== 'email'
  168. ? `Updating email address for ${getProviderName(selectedProviderUpdateEmail ?? '')} identity`
  169. : 'Update email address'}
  170. </DialogTitle>
  171. </DialogHeader>
  172. {selectedProviderUpdateEmail === 'github' ? (
  173. <GitHubChangeEmailAddress />
  174. ) : selectedProviderUpdateEmail?.startsWith('sso') ? (
  175. <SSOChangeEmailAddress />
  176. ) : (
  177. <ChangeEmailAddressForm onClose={() => setSelectedProviderUpdateEmail(undefined)} />
  178. )}
  179. </DialogContent>
  180. </Dialog>
  181. <ConfirmationModal
  182. variant="warning"
  183. size="small"
  184. loading={isUnlinking}
  185. visible={!!selectedProviderUnlink}
  186. title={`Unlink ${getProviderName(selectedProviderUnlink ?? '')} identity`}
  187. onCancel={() => setSelectedProviderUnlink(undefined)}
  188. onConfirm={onConfirmUnlinkIdentity}
  189. confirmLabel="Unlink identity"
  190. confirmLabelLoading="Unlinking identity"
  191. alert={{
  192. base: { variant: 'warning' },
  193. title: `Confirm to disconnect your ${getProviderName(selectedProviderUnlink ?? '')} identity`,
  194. description: `After disconnecting, you will only be able to sign in via ${selectedProviderUnlink === 'github' ? 'email and password' : 'your GitHub identity'}`,
  195. }}
  196. />
  197. </PageSectionContent>
  198. </PageSection>
  199. )
  200. }