Redesign broker connection flow
This commit is contained in:
parent
56f6e8551e
commit
af5e0a50a8
@ -1,7 +1,15 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { ArrowUpRight, PlugZap, RefreshCcw, ShieldCheck } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
ChevronLeft,
|
||||
PlugZap,
|
||||
RefreshCcw,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@ -41,9 +49,45 @@ type HoldingsResponse = {
|
||||
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_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() {
|
||||
const url = new URL(`${window.location.origin}/login`);
|
||||
url.searchParams.set("flow", "reconnect");
|
||||
@ -57,6 +101,58 @@ function formatBrokerName(broker?: string | null) {
|
||||
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({
|
||||
layout = "desktop",
|
||||
open,
|
||||
@ -73,13 +169,14 @@ export default function BrokerConnectDialog({
|
||||
onOpenChange?.(nextOpen);
|
||||
};
|
||||
const [loginPromptOpen, setLoginPromptOpen] = useState(false);
|
||||
const [selectedBroker, setSelectedBroker] = useState<BrokerKey | null>(null);
|
||||
const [zerodhaApiKey, setZerodhaApiKey] = useState("");
|
||||
const [zerodhaApiSecret, setZerodhaApiSecret] = useState("");
|
||||
const [growwApiKey, setGrowwApiKey] = useState("");
|
||||
const [growwApiSecret, setGrowwApiSecret] = useState("");
|
||||
const [holdings, setHoldings] = useState<any[]>([]);
|
||||
|
||||
const { data: sessionUser, refetch: refetchSessionUser } = useQuery<SessionUser | null>({
|
||||
const { refetch: refetchSessionUser } = useQuery<SessionUser | null>({
|
||||
queryKey: ["/me"],
|
||||
queryFn: getQueryFn<SessionUser | null>({ on401: "returnNull" }),
|
||||
staleTime: 0,
|
||||
@ -94,7 +191,7 @@ export default function BrokerConnectDialog({
|
||||
});
|
||||
|
||||
const connected = !!brokerStatus?.connected;
|
||||
const connectedBroker = (brokerStatus?.broker || "").trim().toUpperCase();
|
||||
const connectedBroker = (brokerStatus?.broker || "").trim().toUpperCase() as BrokerKey | "";
|
||||
const brokerLabel = formatBrokerName(connectedBroker);
|
||||
const connectedAt = brokerStatus?.connected_at ? new Date(brokerStatus.connected_at) : null;
|
||||
const canReconnectWithSavedZerodha = connected && connectedBroker === "ZERODHA";
|
||||
@ -108,6 +205,8 @@ export default function BrokerConnectDialog({
|
||||
: layoutClasses;
|
||||
}, [connected, layout]);
|
||||
|
||||
const selectedChoice = brokerChoices.find((choice) => choice.key === selectedBroker) || null;
|
||||
|
||||
const zerodhaLoginMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!zerodhaApiKey.trim()) {
|
||||
@ -248,7 +347,9 @@ export default function BrokerConnectDialog({
|
||||
setHoldings(data?.holdings || []);
|
||||
toast({
|
||||
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) =>
|
||||
@ -265,6 +366,7 @@ export default function BrokerConnectDialog({
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await refetchStatus();
|
||||
setSelectedBroker(null);
|
||||
toast({
|
||||
title: "Broker disconnected",
|
||||
description: "Your broker has been unlinked.",
|
||||
@ -289,6 +391,18 @@ export default function BrokerConnectDialog({
|
||||
setConnectOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!connectOpen) {
|
||||
setSelectedBroker(null);
|
||||
return;
|
||||
}
|
||||
if (connectedBroker === "ZERODHA" || connectedBroker === "GROWW") {
|
||||
setSelectedBroker(connectedBroker);
|
||||
return;
|
||||
}
|
||||
setSelectedBroker(null);
|
||||
}, [connectOpen, connectedBroker]);
|
||||
|
||||
useEffect(() => {
|
||||
const consumeCallback = (rawValue?: string) => {
|
||||
const value = rawValue || localStorage.getItem(CALLBACK_STORAGE_KEY);
|
||||
@ -335,6 +449,172 @@ export default function BrokerConnectDialog({
|
||||
return () => window.removeEventListener("storage", handleStorage);
|
||||
}, [refetchStatus]);
|
||||
|
||||
const renderSelectedBrokerForm = () => {
|
||||
if (!selectedChoice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selectedChoice.key === "ZERODHA") {
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
<div className="relative overflow-hidden rounded-2xl border border-cyan-400/30 bg-background/80 p-5">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-cyan-500/20 via-transparent to-transparent" />
|
||||
<div className="relative flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="rounded-full bg-cyan-500/15 p-2 text-cyan-200">
|
||||
<PlugZap className="h-4 w-4" />
|
||||
</div>
|
||||
<p className="text-lg font-semibold">Zerodha approval</p>
|
||||
</div>
|
||||
<p className="max-w-2xl text-sm leading-6 text-muted-foreground">
|
||||
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>
|
||||
</div>
|
||||
<Badge className="border-cyan-400/40 bg-cyan-500/10 text-cyan-200">Live flow</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="zerodha-api-key">API key</Label>
|
||||
<Input
|
||||
id="zerodha-api-key"
|
||||
placeholder="Enter Zerodha API key"
|
||||
value={zerodhaApiKey}
|
||||
onChange={(e) => setZerodhaApiKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="zerodha-api-secret">API secret</Label>
|
||||
<Input
|
||||
id="zerodha-api-secret"
|
||||
placeholder="Enter Zerodha API secret"
|
||||
type="password"
|
||||
value={zerodhaApiSecret}
|
||||
onChange={(e) => setZerodhaApiSecret(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => zerodhaLoginMutation.mutate()}
|
||||
disabled={zerodhaLoginMutation.isPending}
|
||||
>
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
{zerodhaLoginMutation.isPending ? "Opening Zerodha..." : "Open Zerodha login"}
|
||||
</Button>
|
||||
{canReconnectWithSavedZerodha ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => zerodhaReconnectMutation.mutate()}
|
||||
disabled={zerodhaReconnectMutation.isPending}
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
{zerodhaReconnectMutation.isPending ? "Opening Zerodha..." : "Reconnect saved Zerodha"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-border/60 bg-background/60 p-4 text-xs leading-6 text-muted-foreground">
|
||||
Keep this tab open until the broker login completes. Zerodha access tokens expire daily, so reconnect
|
||||
may be required after market sessions.
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key="groww-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"
|
||||
>
|
||||
<div className="relative overflow-hidden rounded-2xl border border-emerald-400/30 bg-background/80 p-5">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-emerald-500/20 via-transparent to-transparent" />
|
||||
<div className="relative flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-2">
|
||||
<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>
|
||||
</div>
|
||||
<Badge className="border-emerald-400/40 bg-emerald-500/10 text-emerald-200">Direct API</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="groww-api-key">API key</Label>
|
||||
<Input
|
||||
id="groww-api-key"
|
||||
placeholder="Enter Groww API key"
|
||||
value={growwApiKey}
|
||||
onChange={(e) => setGrowwApiKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="groww-api-secret">API secret</Label>
|
||||
<Input
|
||||
id="groww-api-secret"
|
||||
placeholder="Enter Groww API secret"
|
||||
type="password"
|
||||
value={growwApiSecret}
|
||||
onChange={(e) => setGrowwApiSecret(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => growwConnectMutation.mutate()}
|
||||
disabled={growwConnectMutation.isPending}
|
||||
>
|
||||
<PlugZap className="h-4 w-4" />
|
||||
{growwConnectMutation.isPending ? "Connecting Groww..." : "Connect Groww"}
|
||||
</Button>
|
||||
{canReconnectWithSavedGroww ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => growwReconnectMutation.mutate()}
|
||||
disabled={growwReconnectMutation.isPending}
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
{growwReconnectMutation.isPending ? "Refreshing Groww..." : "Reconnect saved Groww"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-border/60 bg-background/60 p-4 text-xs leading-6 text-muted-foreground">
|
||||
Groww reconnect uses your saved approval data on the server, so the portfolio can refresh access without
|
||||
opening another tab after the first setup.
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={connectOpen} onOpenChange={setConnectOpen}>
|
||||
@ -347,35 +627,67 @@ export default function BrokerConnectDialog({
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<motion.div
|
||||
className="space-y-4"
|
||||
className="relative space-y-5"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
transition={{ duration: 0.45, ease: "easeOut" }}
|
||||
>
|
||||
<DialogHeader className="space-y-2">
|
||||
<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>
|
||||
Link your brokerage to pull positions and keep your dashboard in sync.
|
||||
Choose a broker first, then complete a guided setup flow instead of juggling every provider at once.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<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="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">
|
||||
<PlugZap className="h-4 w-4" />
|
||||
<Sparkles className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="text-sm leading-tight">
|
||||
<div className="space-y-1 text-sm">
|
||||
<p className="font-medium">Secure brokerage linking</p>
|
||||
<p className="text-muted-foreground">
|
||||
Connect with Zerodha through the broker login flow or use Groww with direct API approval.
|
||||
<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>
|
||||
<Badge variant={connected ? "secondary" : "outline"} className="ml-auto">
|
||||
{connected ? "Connected" : "Not connected"}
|
||||
</Badge>
|
||||
</div>
|
||||
{connected ? (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
@ -392,156 +704,70 @@ export default function BrokerConnectDialog({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 xl: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.
|
||||
<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>
|
||||
<Badge variant="secondary">Live</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="zerodha-api-key">API key</Label>
|
||||
<Input
|
||||
id="zerodha-api-key"
|
||||
placeholder="Enter Zerodha API key"
|
||||
value={zerodhaApiKey}
|
||||
onChange={(e) => setZerodhaApiKey(e.target.value)}
|
||||
/>
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
{brokerChoices.map((choice) => (
|
||||
<BrokerChoiceCard
|
||||
key={choice.key}
|
||||
choice={choice}
|
||||
onSelect={setSelectedBroker}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="zerodha-api-secret">API secret</Label>
|
||||
<Input
|
||||
id="zerodha-api-secret"
|
||||
placeholder="Enter Zerodha API secret"
|
||||
type="password"
|
||||
value={zerodhaApiSecret}
|
||||
onChange={(e) => setZerodhaApiSecret(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => zerodhaLoginMutation.mutate()}
|
||||
disabled={zerodhaLoginMutation.isPending}
|
||||
>
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
{zerodhaLoginMutation.isPending ? "Opening Zerodha..." : "Open Zerodha login"}
|
||||
</Button>
|
||||
{canReconnectWithSavedZerodha ? (
|
||||
<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">ICICI Direct (coming soon)</Badge>
|
||||
<Badge variant="outline">HDFC Securities (coming soon)</Badge>
|
||||
</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="secondary"
|
||||
onClick={() => zerodhaReconnectMutation.mutate()}
|
||||
disabled={zerodhaReconnectMutation.isPending}
|
||||
variant="ghost"
|
||||
className="h-auto px-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
|
||||
onClick={() => setSelectedBroker(null)}
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
{zerodhaReconnectMutation.isPending ? "Opening Zerodha..." : "Reconnect saved Zerodha"}
|
||||
<ChevronLeft className="mr-1 h-4 w-4" />
|
||||
Back to broker selection
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
After you log in with your Zerodha account, we will connect automatically. Keep this tab open
|
||||
until the login completes.
|
||||
</p>
|
||||
<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
|
||||
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">Groww</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Provide your Groww API key and secret to generate a direct broker session for holdings and live execution.
|
||||
</p>
|
||||
<Badge className={selectedChoice?.accentClass}>{selectedChoice?.title}</Badge>
|
||||
</div>
|
||||
<Badge variant="secondary">Live</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="groww-api-key">API key</Label>
|
||||
<Input
|
||||
id="groww-api-key"
|
||||
placeholder="Enter Groww API key"
|
||||
value={growwApiKey}
|
||||
onChange={(e) => setGrowwApiKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="groww-api-secret">API secret</Label>
|
||||
<Input
|
||||
id="groww-api-secret"
|
||||
placeholder="Enter Groww API secret"
|
||||
type="password"
|
||||
value={growwApiSecret}
|
||||
onChange={(e) => setGrowwApiSecret(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => growwConnectMutation.mutate()}
|
||||
disabled={growwConnectMutation.isPending}
|
||||
>
|
||||
<PlugZap className="h-4 w-4" />
|
||||
{growwConnectMutation.isPending ? "Connecting Groww..." : "Connect Groww"}
|
||||
</Button>
|
||||
{canReconnectWithSavedGroww ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => growwReconnectMutation.mutate()}
|
||||
disabled={growwReconnectMutation.isPending}
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
{growwReconnectMutation.isPending ? "Refreshing Groww..." : "Reconnect saved Groww"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Groww reconnect runs server-side using your saved API key and secret. No browser callback is required after the first setup.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 rounded-lg border border-dashed border-border/50 p-4 text-sm text-muted-foreground">
|
||||
<p className="font-medium text-foreground">Other brokers</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">Angel One (coming soon)</Badge>
|
||||
<Badge variant="outline">ICICI Direct (coming soon)</Badge>
|
||||
<Badge variant="outline">HDFC Securities (coming soon)</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{renderSelectedBrokerForm()}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{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 items-center gap-2">
|
||||
<ShieldCheck className="h-4 w-4 text-primary" />
|
||||
@ -565,6 +791,7 @@ export default function BrokerConnectDialog({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{holdings.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No holdings pulled yet. Click “Fetch holdings” after connecting.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user