diff --git a/client/components/settings/OAuthConnections.tsx b/client/components/settings/OAuthConnections.tsx new file mode 100644 index 00000000..d0847173 --- /dev/null +++ b/client/components/settings/OAuthConnections.tsx @@ -0,0 +1,162 @@ +import { memo } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import { Loader2, Link as LinkIcon, Unlink, ShieldCheck } from "lucide-react"; + +export type ProviderKey = "google" | "github"; + +export interface ProviderDescriptor { + provider: ProviderKey; + name: string; + description: string; + Icon: React.ComponentType<{ className?: string }>; + gradient: string; +} + +export interface LinkedProviderMeta { + provider: ProviderKey; + identityId?: string; + linkedAt?: string; + lastSignInAt?: string; +} + +interface OAuthConnectionsProps { + providers: readonly ProviderDescriptor[]; + linkedProviderMap: Record; + connectionAction: string | null; + onLink: (provider: ProviderKey) => void; + onUnlink: (provider: ProviderKey) => void; + ensureSecondarySignIn?: boolean; +} + +const formatTimestamp = (value?: string) => { + if (!value) return null; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return null; + try { + return new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "short", + }).format(date); + } catch { + return date.toLocaleString(); + } +}; + +const statusCopy = { + linked: "Linked", + notLinked: "Not linked yet", +}; + +const OAuthConnections = memo(function OAuthConnections({ + providers, + linkedProviderMap, + connectionAction, + onLink, + onUnlink, +}: OAuthConnectionsProps) { + return ( +
+ {providers.map((providerConfig) => { + const { provider, name, description, Icon, gradient } = providerConfig; + const linkedMeta = linkedProviderMap[provider]; + const isLinking = connectionAction === `${provider}-link`; + const isUnlinking = connectionAction === `${provider}-unlink`; + + const linkedBadge = linkedMeta ? ( + + + {statusCopy.linked} + + ) : ( + + {statusCopy.notLinked} + + ); + + return ( +
+
+
+ +
+
+
+

{name}

+ {linkedBadge} +
+

{description}

+ {linkedMeta && ( +
+ {linkedMeta.linkedAt && ( +
+ Linked:{" "} + {formatTimestamp(linkedMeta.linkedAt)} +
+ )} + {linkedMeta.lastSignInAt && ( +
+ Last sign-in:{" "} + {formatTimestamp(linkedMeta.lastSignInAt)} +
+ )} + {linkedMeta.identityId && ( +
+ Identity:{" "} + {linkedMeta.identityId} +
+ )} +
+ )} +
+
+
+ {linkedMeta ? ( + + ) : ( + + )} +
+
+ ); + })} +
+ ); +}); + +export default OAuthConnections;