AccountIdentities.tsx213 lines · main
| 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 | |
| 33 | import { |
| 34 | ChangeEmailAddressForm, |
| 35 | GitHubChangeEmailAddress, |
| 36 | SSOChangeEmailAddress, |
| 37 | } from './ChangeEmailAddress' |
| 38 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 39 | import { useProfileIdentitiesQuery } from '@/data/profile/profile-identities-query' |
| 40 | import { useUnlinkIdentityMutation } from '@/data/profile/profile-unlink-identity-mutation' |
| 41 | import { BASE_PATH } from '@/lib/constants' |
| 42 | |
| 43 | const getProviderName = (provider: string) => |
| 44 | provider === 'github' |
| 45 | ? 'GitHub' |
| 46 | : provider.startsWith('sso') |
| 47 | ? 'SSO' |
| 48 | : provider.replaceAll('_', ' ') |
| 49 | |
| 50 | export 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 | } |