Redesign broker connection flow

This commit is contained in:
Thigazhezhilan J 2026-04-05 20:00:43 +05:30
parent 56f6e8551e
commit af5e0a50a8

View File

@ -1,7 +1,15 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { ArrowUpRight, PlugZap, RefreshCcw, ShieldCheck } from "lucide-react"; import {
import { motion } from "framer-motion"; ArrowRight,
ArrowUpRight,
ChevronLeft,
PlugZap,
RefreshCcw,
ShieldCheck,
Sparkles,
} from "lucide-react";
import { AnimatePresence, motion } from "framer-motion";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -41,9 +49,45 @@ type HoldingsResponse = {
holdings?: any[]; holdings?: any[];
}; };
type BrokerKey = "ZERODHA" | "GROWW";
type BrokerChoice = {
key: BrokerKey;
title: string;
subtitle: string;
accentClass: string;
glowClass: string;
bulletOne: string;
bulletTwo: string;
connectLabel: string;
};
const CALLBACK_STORAGE_KEY = "zerodha:callback"; const CALLBACK_STORAGE_KEY = "zerodha:callback";
const CALLBACK_MAX_AGE_MS = 5 * 60 * 1000; const CALLBACK_MAX_AGE_MS = 5 * 60 * 1000;
const brokerChoices: BrokerChoice[] = [
{
key: "ZERODHA",
title: "Zerodha",
subtitle: "Use the Kite login flow and finish the broker approval in a new tab.",
accentClass: "border-cyan-400/40 bg-cyan-500/10 text-cyan-200",
glowClass: "from-cyan-500/20 via-cyan-500/5 to-transparent",
bulletOne: "Browser-based login approval",
bulletTwo: "Saved credential reconnect after first setup",
connectLabel: "Continue with Zerodha",
},
{
key: "GROWW",
title: "Groww",
subtitle: "Use direct API approval and create a live broker session without a callback step.",
accentClass: "border-emerald-400/40 bg-emerald-500/10 text-emerald-200",
glowClass: "from-emerald-500/20 via-emerald-500/5 to-transparent",
bulletOne: "Direct API key and secret approval",
bulletTwo: "Server-side reconnect using saved approval",
connectLabel: "Continue with Groww",
},
];
function buildReconnectRedirectUrl() { function buildReconnectRedirectUrl() {
const url = new URL(`${window.location.origin}/login`); const url = new URL(`${window.location.origin}/login`);
url.searchParams.set("flow", "reconnect"); url.searchParams.set("flow", "reconnect");
@ -57,6 +101,58 @@ function formatBrokerName(broker?: string | null) {
return normalized || "broker"; return normalized || "broker";
} }
function BrokerChoiceCard({
choice,
onSelect,
}: {
choice: BrokerChoice;
onSelect: (broker: BrokerKey) => void;
}) {
return (
<motion.button
type="button"
onClick={() => onSelect(choice.key)}
className="group relative overflow-hidden rounded-2xl border border-border/60 bg-background/70 p-5 text-left shadow-lg transition-colors hover:border-primary/50"
whileHover={{
y: -8,
boxShadow: "0 24px 60px rgba(0,0,0,0.22)",
}}
whileTap={{ scale: 0.99 }}
transition={{ type: "spring", stiffness: 320, damping: 24 }}
>
<div className={`pointer-events-none absolute inset-0 bg-gradient-to-br ${choice.glowClass} opacity-90`} />
<div className="relative space-y-5">
<div className="flex items-start justify-between gap-3">
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="rounded-full bg-primary/15 p-2 text-primary">
<PlugZap className="h-4 w-4" />
</div>
<p className="text-lg font-semibold">{choice.title}</p>
</div>
<p className="max-w-sm text-sm leading-6 text-muted-foreground">{choice.subtitle}</p>
</div>
<Badge className={choice.accentClass}>Live</Badge>
</div>
<div className="grid gap-2 text-sm text-muted-foreground">
<div className="rounded-xl border border-white/5 bg-background/65 px-3 py-2">
{choice.bulletOne}
</div>
<div className="rounded-xl border border-white/5 bg-background/65 px-3 py-2">
{choice.bulletTwo}
</div>
</div>
<div className="flex items-center justify-between rounded-xl border border-primary/20 bg-primary/10 px-3 py-3 text-sm font-medium text-primary">
<span>{choice.connectLabel}</span>
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</div>
</div>
</motion.button>
);
}
export default function BrokerConnectDialog({ export default function BrokerConnectDialog({
layout = "desktop", layout = "desktop",
open, open,
@ -73,13 +169,14 @@ export default function BrokerConnectDialog({
onOpenChange?.(nextOpen); onOpenChange?.(nextOpen);
}; };
const [loginPromptOpen, setLoginPromptOpen] = useState(false); const [loginPromptOpen, setLoginPromptOpen] = useState(false);
const [selectedBroker, setSelectedBroker] = useState<BrokerKey | null>(null);
const [zerodhaApiKey, setZerodhaApiKey] = useState(""); const [zerodhaApiKey, setZerodhaApiKey] = useState("");
const [zerodhaApiSecret, setZerodhaApiSecret] = useState(""); const [zerodhaApiSecret, setZerodhaApiSecret] = useState("");
const [growwApiKey, setGrowwApiKey] = useState(""); const [growwApiKey, setGrowwApiKey] = useState("");
const [growwApiSecret, setGrowwApiSecret] = useState(""); const [growwApiSecret, setGrowwApiSecret] = useState("");
const [holdings, setHoldings] = useState<any[]>([]); const [holdings, setHoldings] = useState<any[]>([]);
const { data: sessionUser, refetch: refetchSessionUser } = useQuery<SessionUser | null>({ const { refetch: refetchSessionUser } = useQuery<SessionUser | null>({
queryKey: ["/me"], queryKey: ["/me"],
queryFn: getQueryFn<SessionUser | null>({ on401: "returnNull" }), queryFn: getQueryFn<SessionUser | null>({ on401: "returnNull" }),
staleTime: 0, staleTime: 0,
@ -94,7 +191,7 @@ export default function BrokerConnectDialog({
}); });
const connected = !!brokerStatus?.connected; const connected = !!brokerStatus?.connected;
const connectedBroker = (brokerStatus?.broker || "").trim().toUpperCase(); const connectedBroker = (brokerStatus?.broker || "").trim().toUpperCase() as BrokerKey | "";
const brokerLabel = formatBrokerName(connectedBroker); const brokerLabel = formatBrokerName(connectedBroker);
const connectedAt = brokerStatus?.connected_at ? new Date(brokerStatus.connected_at) : null; const connectedAt = brokerStatus?.connected_at ? new Date(brokerStatus.connected_at) : null;
const canReconnectWithSavedZerodha = connected && connectedBroker === "ZERODHA"; const canReconnectWithSavedZerodha = connected && connectedBroker === "ZERODHA";
@ -108,6 +205,8 @@ export default function BrokerConnectDialog({
: layoutClasses; : layoutClasses;
}, [connected, layout]); }, [connected, layout]);
const selectedChoice = brokerChoices.find((choice) => choice.key === selectedBroker) || null;
const zerodhaLoginMutation = useMutation({ const zerodhaLoginMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
if (!zerodhaApiKey.trim()) { if (!zerodhaApiKey.trim()) {
@ -248,7 +347,9 @@ export default function BrokerConnectDialog({
setHoldings(data?.holdings || []); setHoldings(data?.holdings || []);
toast({ toast({
title: "Holdings fetched", title: "Holdings fetched",
description: `Latest positions pulled from ${formatBrokerName(data?.broker || brokerStatus?.broker)}.`, description: `Latest positions pulled from ${formatBrokerName(
data?.broker || brokerStatus?.broker,
)}.`,
}); });
}, },
onError: (err: any) => onError: (err: any) =>
@ -265,6 +366,7 @@ export default function BrokerConnectDialog({
}, },
onSuccess: async () => { onSuccess: async () => {
await refetchStatus(); await refetchStatus();
setSelectedBroker(null);
toast({ toast({
title: "Broker disconnected", title: "Broker disconnected",
description: "Your broker has been unlinked.", description: "Your broker has been unlinked.",
@ -289,6 +391,18 @@ export default function BrokerConnectDialog({
setConnectOpen(true); setConnectOpen(true);
}; };
useEffect(() => {
if (!connectOpen) {
setSelectedBroker(null);
return;
}
if (connectedBroker === "ZERODHA" || connectedBroker === "GROWW") {
setSelectedBroker(connectedBroker);
return;
}
setSelectedBroker(null);
}, [connectOpen, connectedBroker]);
useEffect(() => { useEffect(() => {
const consumeCallback = (rawValue?: string) => { const consumeCallback = (rawValue?: string) => {
const value = rawValue || localStorage.getItem(CALLBACK_STORAGE_KEY); const value = rawValue || localStorage.getItem(CALLBACK_STORAGE_KEY);
@ -335,83 +449,41 @@ export default function BrokerConnectDialog({
return () => window.removeEventListener("storage", handleStorage); return () => window.removeEventListener("storage", handleStorage);
}, [refetchStatus]); }, [refetchStatus]);
const renderSelectedBrokerForm = () => {
if (!selectedChoice) {
return null;
}
if (selectedChoice.key === "ZERODHA") {
return ( return (
<>
<Dialog open={connectOpen} onOpenChange={setConnectOpen}>
<Button
variant="secondary"
className={[triggerClassName, triggerClassNameProp].filter(Boolean).join(" ")}
disabled={connected}
onClick={handleConnectClick}
>
<PlugZap className="h-4 w-4" />
{connected ? "Broker connected" : "Connect broker"}
</Button>
<DialogContent className="sm:max-w-3xl border-border/70 bg-gradient-to-br from-background via-background to-muted/30">
<motion.div <motion.div
key="zerodha-step"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.28, ease: "easeOut" }}
className="space-y-4" className="space-y-4"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
> >
<DialogHeader className="space-y-2"> <div className="relative overflow-hidden rounded-2xl border border-cyan-400/30 bg-background/80 p-5">
<DialogTitle>Connect your broker</DialogTitle> <div className="absolute inset-0 bg-gradient-to-br from-cyan-500/20 via-transparent to-transparent" />
<DialogDescription> <div className="relative flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
Link your brokerage to pull positions and keep your dashboard in sync. <div className="space-y-2">
</DialogDescription> <div className="flex items-center gap-2">
</DialogHeader> <div className="rounded-full bg-cyan-500/15 p-2 text-cyan-200">
<div className="space-y-4 rounded-xl border border-border/70 bg-card/70 p-4">
<div className="flex flex-col gap-3 rounded-lg border border-border/60 bg-gradient-to-r from-primary/10 via-background to-background px-3 py-3">
<div className="flex items-center gap-3">
<div className="rounded-full bg-primary/15 p-2 text-primary">
<PlugZap className="h-4 w-4" /> <PlugZap className="h-4 w-4" />
</div> </div>
<div className="text-sm leading-tight"> <p className="text-lg font-semibold">Zerodha approval</p>
<p className="font-medium">Secure brokerage linking</p> </div>
<p className="text-muted-foreground"> <p className="max-w-2xl text-sm leading-6 text-muted-foreground">
Connect with Zerodha through the broker login flow or use Groww with direct API approval. Enter your Kite API key and secret, then continue in Zerodha to approve the session. After the
first setup, reconnect can reuse the saved credentials.
</p> </p>
</div> </div>
<Badge variant={connected ? "secondary" : "outline"} className="ml-auto"> <Badge className="border-cyan-400/40 bg-cyan-500/10 text-cyan-200">Live flow</Badge>
{connected ? "Connected" : "Not connected"}
</Badge>
</div> </div>
{connected ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<ShieldCheck className="h-4 w-4 text-primary" />
<span>
{brokerStatus?.userName
? `Linked as ${brokerStatus.userName}`
: `Linked to ${brokerLabel}`}{" "}
via {brokerLabel}
{" - "}
{connectedAt ? connectedAt.toLocaleString() : "just now"}
</span>
</div>
) : null}
</div> </div>
<div className="grid gap-4 xl:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<motion.div
className="space-y-3 rounded-lg border border-border/60 bg-background/60 p-4 shadow-sm"
whileHover={{
y: -6,
boxShadow: "0 20px 40px rgba(0,0,0,0.18)",
}}
transition={{ type: "spring", stiffness: 300, damping: 24 }}
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-semibold">Zerodha</p>
<p className="text-xs text-muted-foreground">
Provide your Kite API key and secret to launch the login and connect your account.
</p>
</div>
<Badge variant="secondary">Live</Badge>
</div>
<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="zerodha-api-key">API key</Label> <Label htmlFor="zerodha-api-key">API key</Label>
<Input <Input
@ -433,9 +505,9 @@ export default function BrokerConnectDialog({
</div> </div>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<Button <Button
variant="outline" className="w-full sm:w-auto"
onClick={() => zerodhaLoginMutation.mutate()} onClick={() => zerodhaLoginMutation.mutate()}
disabled={zerodhaLoginMutation.isPending} disabled={zerodhaLoginMutation.isPending}
> >
@ -445,6 +517,7 @@ export default function BrokerConnectDialog({
{canReconnectWithSavedZerodha ? ( {canReconnectWithSavedZerodha ? (
<Button <Button
variant="secondary" variant="secondary"
className="w-full sm:w-auto"
onClick={() => zerodhaReconnectMutation.mutate()} onClick={() => zerodhaReconnectMutation.mutate()}
disabled={zerodhaReconnectMutation.isPending} disabled={zerodhaReconnectMutation.isPending}
> >
@ -454,35 +527,43 @@ export default function BrokerConnectDialog({
) : null} ) : null}
</div> </div>
<p className="text-xs text-muted-foreground"> <div className="rounded-2xl border border-border/60 bg-background/60 p-4 text-xs leading-6 text-muted-foreground">
After you log in with your Zerodha account, we will connect automatically. Keep this tab open Keep this tab open until the broker login completes. Zerodha access tokens expire daily, so reconnect
until the login completes. may be required after market sessions.
</p> </div>
<p className="text-xs text-muted-foreground">
Zerodha access tokens expire daily. After the first setup, reconnect can use your saved API key
and secret without re-entering them.
</p>
</motion.div> </motion.div>
);
}
return (
<motion.div <motion.div
className="space-y-3 rounded-lg border border-border/60 bg-background/60 p-4 shadow-sm" key="groww-step"
whileHover={{ initial={{ opacity: 0, x: 20 }}
y: -6, animate={{ opacity: 1, x: 0 }}
boxShadow: "0 20px 40px rgba(0,0,0,0.18)", exit={{ opacity: 0, x: -20 }}
}} transition={{ duration: 0.28, ease: "easeOut" }}
transition={{ type: "spring", stiffness: 300, damping: 24 }} className="space-y-4"
> >
<div className="flex items-center justify-between"> <div className="relative overflow-hidden rounded-2xl border border-emerald-400/30 bg-background/80 p-5">
<div> <div className="absolute inset-0 bg-gradient-to-br from-emerald-500/20 via-transparent to-transparent" />
<p className="text-sm font-semibold">Groww</p> <div className="relative flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<p className="text-xs text-muted-foreground"> <div className="space-y-2">
Provide your Groww API key and secret to generate a direct broker session for holdings and live execution. <div className="flex items-center gap-2">
<div className="rounded-full bg-emerald-500/15 p-2 text-emerald-200">
<Sparkles className="h-4 w-4" />
</div>
<p className="text-lg font-semibold">Groww approval</p>
</div>
<p className="max-w-2xl text-sm leading-6 text-muted-foreground">
Enter your Groww API key and secret to create the broker session directly. Reconnect runs
server-side, so you can refresh the session without another browser callback step.
</p> </p>
</div> </div>
<Badge variant="secondary">Live</Badge> <Badge className="border-emerald-400/40 bg-emerald-500/10 text-emerald-200">Direct API</Badge>
</div>
</div> </div>
<div className="grid gap-3 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="groww-api-key">API key</Label> <Label htmlFor="groww-api-key">API key</Label>
<Input <Input
@ -504,9 +585,9 @@ export default function BrokerConnectDialog({
</div> </div>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<Button <Button
variant="outline" className="w-full sm:w-auto"
onClick={() => growwConnectMutation.mutate()} onClick={() => growwConnectMutation.mutate()}
disabled={growwConnectMutation.isPending} disabled={growwConnectMutation.isPending}
> >
@ -516,6 +597,7 @@ export default function BrokerConnectDialog({
{canReconnectWithSavedGroww ? ( {canReconnectWithSavedGroww ? (
<Button <Button
variant="secondary" variant="secondary"
className="w-full sm:w-auto"
onClick={() => growwReconnectMutation.mutate()} onClick={() => growwReconnectMutation.mutate()}
disabled={growwReconnectMutation.isPending} disabled={growwReconnectMutation.isPending}
> >
@ -525,23 +607,167 @@ export default function BrokerConnectDialog({
) : null} ) : null}
</div> </div>
<p className="text-xs text-muted-foreground"> <div className="rounded-2xl border border-border/60 bg-background/60 p-4 text-xs leading-6 text-muted-foreground">
Groww reconnect runs server-side using your saved API key and secret. No browser callback is required after the first setup. Groww reconnect uses your saved approval data on the server, so the portfolio can refresh access without
</p> opening another tab after the first setup.
</div>
</motion.div> </motion.div>
);
};
return (
<>
<Dialog open={connectOpen} onOpenChange={setConnectOpen}>
<Button
variant="secondary"
className={[triggerClassName, triggerClassNameProp].filter(Boolean).join(" ")}
disabled={connected}
onClick={handleConnectClick}
>
<PlugZap className="h-4 w-4" />
{connected ? "Broker connected" : "Connect broker"}
</Button>
<DialogContent className="overflow-hidden border-border/70 bg-gradient-to-br from-background via-background to-muted/30 sm:max-w-3xl">
<div className="pointer-events-none absolute inset-0">
<div className="absolute -right-24 top-0 h-64 w-64 rounded-full bg-primary/10 blur-3xl" />
<div className="absolute left-0 top-40 h-56 w-56 rounded-full bg-chart-2/10 blur-3xl" />
</div> </div>
<div className="space-y-2 rounded-lg border border-dashed border-border/50 p-4 text-sm text-muted-foreground"> <motion.div
<p className="font-medium text-foreground">Other brokers</p> className="relative space-y-5"
<div className="flex flex-wrap gap-2"> initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, ease: "easeOut" }}
>
<DialogHeader className="space-y-3">
<div className="flex flex-wrap items-center gap-2">
<Badge className="border-primary/30 bg-primary/10 text-primary">Broker setup</Badge>
<Badge variant={connected ? "secondary" : "outline"}>
{connected ? `${brokerLabel} connected` : "No broker connected"}
</Badge>
</div>
<DialogTitle>Connect your broker</DialogTitle>
<DialogDescription>
Choose a broker first, then complete a guided setup flow instead of juggling every provider at once.
</DialogDescription>
</DialogHeader>
<div className="grid gap-3 rounded-2xl border border-border/60 bg-card/60 p-3 sm:grid-cols-[1fr_1fr]">
<div
className={`rounded-xl px-4 py-3 text-sm ${
selectedBroker
? "border border-border/60 bg-background/40 text-muted-foreground"
: "border border-primary/30 bg-primary/10 text-primary"
}`}
>
<p className="text-xs uppercase tracking-[0.18em]">Step 1</p>
<p className="mt-1 font-medium">Choose your broker</p>
</div>
<div
className={`rounded-xl px-4 py-3 text-sm ${
selectedBroker
? "border border-primary/30 bg-primary/10 text-primary"
: "border border-border/60 bg-background/40 text-muted-foreground"
}`}
>
<p className="text-xs uppercase tracking-[0.18em]">Step 2</p>
<p className="mt-1 font-medium">Enter broker details</p>
</div>
</div>
<div className="rounded-2xl border border-border/60 bg-card/70 p-4">
<div className="mb-4 flex flex-col gap-3 rounded-2xl border border-border/60 bg-gradient-to-r from-primary/10 via-background to-background px-4 py-4">
<div className="flex items-start gap-3">
<div className="rounded-full bg-primary/15 p-2 text-primary">
<Sparkles className="h-4 w-4" />
</div>
<div className="space-y-1 text-sm">
<p className="font-medium">Secure brokerage linking</p>
<p className="leading-6 text-muted-foreground">
The setup keeps your execution on the broker side while QuantFortune only syncs holdings,
funds, and live strategy status.
</p>
</div>
</div>
{connected ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<ShieldCheck className="h-4 w-4 text-primary" />
<span>
{brokerStatus?.userName
? `Linked as ${brokerStatus.userName}`
: `Linked to ${brokerLabel}`}{" "}
via {brokerLabel}
{" - "}
{connectedAt ? connectedAt.toLocaleString() : "just now"}
</span>
</div>
) : null}
</div>
<AnimatePresence mode="wait">
{!selectedBroker ? (
<motion.div
key="broker-picker"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.28, ease: "easeOut" }}
className="space-y-5"
>
<div className="space-y-1">
<p className="text-lg font-semibold">Choose a broker to connect</p>
<p className="text-sm text-muted-foreground">
Pick the broker you want to authorize now. You can change or reconnect later from the same dashboard.
</p>
</div>
<div className="grid gap-4 xl:grid-cols-2">
{brokerChoices.map((choice) => (
<BrokerChoiceCard
key={choice.key}
choice={choice}
onSelect={setSelectedBroker}
/>
))}
</div>
<div className="rounded-2xl border border-dashed border-border/50 bg-background/50 p-4">
<p className="text-sm font-medium text-foreground">More brokers on the way</p>
<div className="mt-3 flex flex-wrap gap-2 text-sm text-muted-foreground">
<Badge variant="outline">Angel One (coming soon)</Badge> <Badge variant="outline">Angel One (coming soon)</Badge>
<Badge variant="outline">ICICI Direct (coming soon)</Badge> <Badge variant="outline">ICICI Direct (coming soon)</Badge>
<Badge variant="outline">HDFC Securities (coming soon)</Badge> <Badge variant="outline">HDFC Securities (coming soon)</Badge>
</div> </div>
</div> </div>
</motion.div>
) : (
<motion.div
key={`broker-details-${selectedBroker}`}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.28, ease: "easeOut" }}
className="space-y-4"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<Button
variant="ghost"
className="h-auto px-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
onClick={() => setSelectedBroker(null)}
>
<ChevronLeft className="mr-1 h-4 w-4" />
Back to broker selection
</Button>
<Badge className={selectedChoice?.accentClass}>{selectedChoice?.title}</Badge>
</div>
{renderSelectedBrokerForm()}
</motion.div>
)}
</AnimatePresence>
{connected ? ( {connected ? (
<div className="space-y-2 rounded-lg border border-border/60 bg-background/80 p-4"> <div className="mt-5 space-y-3 rounded-2xl border border-border/60 bg-background/80 p-4">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ShieldCheck className="h-4 w-4 text-primary" /> <ShieldCheck className="h-4 w-4 text-primary" />
@ -565,6 +791,7 @@ export default function BrokerConnectDialog({
</Button> </Button>
</div> </div>
</div> </div>
{holdings.length === 0 ? ( {holdings.length === 0 ? (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
No holdings pulled yet. Click &ldquo;Fetch holdings&rdquo; after connecting. No holdings pulled yet. Click &ldquo;Fetch holdings&rdquo; after connecting.