From af5e0a50a8bbe7892681e734e41e30ea46b38f2d Mon Sep 17 00:00:00 2001 From: Thigazhezhilan J Date: Sun, 5 Apr 2026 20:00:43 +0530 Subject: [PATCH] Redesign broker connection flow --- .../landing/BrokerConnectDialog.tsx | 543 +++++++++++++----- 1 file changed, 385 insertions(+), 158 deletions(-) diff --git a/src/components/landing/BrokerConnectDialog.tsx b/src/components/landing/BrokerConnectDialog.tsx index 5409e8b0..b3c15e94 100644 --- a/src/components/landing/BrokerConnectDialog.tsx +++ b/src/components/landing/BrokerConnectDialog.tsx @@ -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 ( + 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 }} + > +
+
+
+
+
+
+ +
+

{choice.title}

+
+

{choice.subtitle}

+
+ Live +
+ +
+
+ {choice.bulletOne} +
+
+ {choice.bulletTwo} +
+
+ +
+ {choice.connectLabel} + +
+
+ + ); +} + 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(null); const [zerodhaApiKey, setZerodhaApiKey] = useState(""); const [zerodhaApiSecret, setZerodhaApiSecret] = useState(""); const [growwApiKey, setGrowwApiKey] = useState(""); const [growwApiSecret, setGrowwApiSecret] = useState(""); const [holdings, setHoldings] = useState([]); - const { data: sessionUser, refetch: refetchSessionUser } = useQuery({ + const { refetch: refetchSessionUser } = useQuery({ queryKey: ["/me"], queryFn: getQueryFn({ 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 ( + +
+
+
+
+
+
+ +
+

Zerodha 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. +

+
+ Live flow +
+
+ +
+
+ + setZerodhaApiKey(e.target.value)} + /> +
+
+ + setZerodhaApiSecret(e.target.value)} + /> +
+
+ +
+ + {canReconnectWithSavedZerodha ? ( + + ) : null} +
+ +
+ Keep this tab open until the broker login completes. Zerodha access tokens expire daily, so reconnect + may be required after market sessions. +
+ + ); + } + + return ( + +
+
+
+
+
+
+ +
+

Groww approval

+
+

+ 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. +

+
+ Direct API +
+
+ +
+
+ + setGrowwApiKey(e.target.value)} + /> +
+
+ + setGrowwApiSecret(e.target.value)} + /> +
+
+ +
+ + {canReconnectWithSavedGroww ? ( + + ) : null} +
+ +
+ Groww reconnect uses your saved approval data on the server, so the portfolio can refresh access without + opening another tab after the first setup. +
+ + ); + }; + return ( <> @@ -347,35 +627,67 @@ export default function BrokerConnectDialog({ {connected ? "Broker connected" : "Connect broker"} - + +
+
+
+
+ - + +
+ Broker setup + + {connected ? `${brokerLabel} connected` : "No broker connected"} + +
Connect your broker - 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.
-
-
-
+
+
+

Step 1

+

Choose your broker

+
+
+

Step 2

+

Enter broker details

+
+
+ +
+
+
- +
-
+

Secure brokerage linking

-

- Connect with Zerodha through the broker login flow or use Groww with direct API approval. +

+ The setup keeps your execution on the broker side while QuantFortune only syncs holdings, + funds, and live strategy status.

- - {connected ? "Connected" : "Not connected"} -
{connected ? (
@@ -392,156 +704,70 @@ export default function BrokerConnectDialog({ ) : null}
-
- -
-
-

Zerodha

-

- Provide your Kite API key and secret to launch the login and connect your account. + + {!selectedBroker ? ( + +

+

Choose a broker to connect

+

+ Pick the broker you want to authorize now. You can change or reconnect later from the same dashboard.

- Live -
-
-
- - setZerodhaApiKey(e.target.value)} - /> +
+ {brokerChoices.map((choice) => ( + + ))}
-
- - setZerodhaApiSecret(e.target.value)} - /> -
-
-
- - {canReconnectWithSavedZerodha ? ( +
+

More brokers on the way

+
+ Angel One (coming soon) + ICICI Direct (coming soon) + HDFC Securities (coming soon) +
+
+ + ) : ( + +
- ) : null} -
- -

- After you log in with your Zerodha account, we will connect automatically. Keep this tab open - until the login completes. -

-

- Zerodha access tokens expire daily. After the first setup, reconnect can use your saved API key - and secret without re-entering them. -

-
- - -
-
-

Groww

-

- Provide your Groww API key and secret to generate a direct broker session for holdings and live execution. -

+ {selectedChoice?.title}
- Live -
-
-
- - setGrowwApiKey(e.target.value)} - /> -
-
- - setGrowwApiSecret(e.target.value)} - /> -
-
- -
- - {canReconnectWithSavedGroww ? ( - - ) : null} -
- -

- Groww reconnect runs server-side using your saved API key and secret. No browser callback is required after the first setup. -

-
-
- -
-

Other brokers

-
- Angel One (coming soon) - ICICI Direct (coming soon) - HDFC Securities (coming soon) -
-
+ {renderSelectedBrokerForm()} + + )} + {connected ? ( -
+
@@ -565,6 +791,7 @@ export default function BrokerConnectDialog({
+ {holdings.length === 0 ? (

No holdings pulled yet. Click “Fetch holdings” after connecting.