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