133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { toast } from "sonner";
|
|
import { ArrowLeftRight, Loader2, Unlink } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { ConfirmDeleteDialog } from "@/components/confirm-delete-dialog";
|
|
import { AccountAvatar } from "@/components/profile/account-avatar";
|
|
import { removeAccountLink, toggleAccount } from "@/lib/actions/account-links";
|
|
import type { LinkedAccountDTO } from "@/types/profile";
|
|
|
|
/**
|
|
* "Linked Accounts": the other accounts this one is linked to.
|
|
* - "Toggle": signs this browser out and back in as that account (the
|
|
* server verifies the link and mints a one-time switch token, so the
|
|
* other account's password is never needed).
|
|
* - "Remove Link": either account in the pair can do it; confirmed first.
|
|
*/
|
|
export function LinkedAccounts({ accounts }: { accounts: LinkedAccountDTO[] }) {
|
|
const [togglingId, setTogglingId] = useState<string | null>(null);
|
|
const [removing, setRemoving] = useState<LinkedAccountDTO | null>(null);
|
|
const [removingBusy, setRemovingBusy] = useState(false);
|
|
|
|
async function handleToggle(account: LinkedAccountDTO) {
|
|
if (togglingId) return;
|
|
setTogglingId(account.id);
|
|
try {
|
|
const result = await toggleAccount(account.id);
|
|
if (result?.url) {
|
|
// The action already signed this browser in as the linked account;
|
|
// navigate so the app shell re-renders for them.
|
|
window.location.href = result.url;
|
|
return;
|
|
}
|
|
toast.error(result?.error ?? "Couldn't switch to that account.");
|
|
} catch {
|
|
toast.error("Couldn't switch to that account. Try again.");
|
|
} finally {
|
|
setTogglingId(null);
|
|
}
|
|
}
|
|
|
|
async function handleRemove() {
|
|
if (!removing) return;
|
|
const account = removing;
|
|
setRemoving(null);
|
|
setRemovingBusy(true);
|
|
try {
|
|
const result = await removeAccountLink(account.linkId);
|
|
if (result?.error) {
|
|
toast.error(result.error);
|
|
return;
|
|
}
|
|
toast.success(`Link with ${account.email} removed.`);
|
|
} catch {
|
|
toast.error("Couldn't remove the link. Try again.");
|
|
} finally {
|
|
setRemovingBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Linked Accounts</CardTitle>
|
|
<CardDescription>
|
|
Accounts this one is linked to. Toggle signs you in as that
|
|
account; Remove Link works from either side.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col gap-3">
|
|
{accounts.map((account) => (
|
|
<div
|
|
key={account.linkId}
|
|
className="flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center"
|
|
>
|
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
<AccountAvatar account={account} sizeClass="size-9" />
|
|
<div className="min-w-0">
|
|
<p className="truncate text-sm font-medium">{account.email}</p>
|
|
{account.name && (
|
|
<p className="truncate text-xs text-muted-foreground">{account.name}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={() => handleToggle(account)}
|
|
disabled={togglingId !== null}
|
|
>
|
|
{togglingId === account.id ? (
|
|
<Loader2 className="size-3.5 animate-spin" />
|
|
) : (
|
|
<ArrowLeftRight className="size-3.5" />
|
|
)}
|
|
{togglingId === account.id ? "Switching…" : "Toggle"}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={togglingId !== null}
|
|
onClick={() => setRemoving(account)}
|
|
>
|
|
<Unlink className="size-3.5" />
|
|
Remove Link
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
|
|
<ConfirmDeleteDialog
|
|
open={removing !== null}
|
|
onOpenChange={(open) => !removingBusy && !open && setRemoving(null)}
|
|
title={`Remove link with ${removing?.email}?`}
|
|
description="Either linked account can remove the link. Removing it doesn't block anything -- the other account can still send a new link request, which you can accept again."
|
|
confirmLabel="Remove Link"
|
|
onConfirm={handleRemove}
|
|
/>
|
|
</Card>
|
|
);
|
|
}
|