Add Groww broker support to portfolio UI

This commit is contained in:
Thigazhezhilan J 2026-04-05 19:42:08 +05:30
parent 71a99b7b0a
commit 56f6e8551e
4 changed files with 437 additions and 227 deletions

View File

@ -1,7 +1,8 @@
import { useEffect, useMemo, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { PlugZap, ArrowUpRight, ShieldCheck, RefreshCcw } from "lucide-react";
import { ArrowUpRight, PlugZap, RefreshCcw, ShieldCheck } from "lucide-react";
import { motion } from "framer-motion";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
@ -10,7 +11,6 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "@/hooks/use-toast";
@ -36,6 +36,11 @@ type BrokerStatusResponse = {
authState?: string;
};
type HoldingsResponse = {
broker?: string;
holdings?: any[];
};
const CALLBACK_STORAGE_KEY = "zerodha:callback";
const CALLBACK_MAX_AGE_MS = 5 * 60 * 1000;
@ -45,6 +50,13 @@ function buildReconnectRedirectUrl() {
return url.toString();
}
function formatBrokerName(broker?: string | null) {
const normalized = (broker || "").trim().toUpperCase();
if (normalized === "GROWW") return "Groww";
if (normalized === "ZERODHA") return "Zerodha";
return normalized || "broker";
}
export default function BrokerConnectDialog({
layout = "desktop",
open,
@ -61,8 +73,10 @@ export default function BrokerConnectDialog({
onOpenChange?.(nextOpen);
};
const [loginPromptOpen, setLoginPromptOpen] = useState(false);
const [apiKey, setApiKey] = useState("");
const [apiSecret, setApiSecret] = useState("");
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>({
@ -79,21 +93,36 @@ export default function BrokerConnectDialog({
refetchOnMount: "always",
});
const loginUrlMutation = useMutation({
const connected = !!brokerStatus?.connected;
const connectedBroker = (brokerStatus?.broker || "").trim().toUpperCase();
const brokerLabel = formatBrokerName(connectedBroker);
const connectedAt = brokerStatus?.connected_at ? new Date(brokerStatus.connected_at) : null;
const canReconnectWithSavedZerodha = connected && connectedBroker === "ZERODHA";
const canReconnectWithSavedGroww = connected && connectedBroker === "GROWW";
const triggerClassName = useMemo(() => {
const layoutClasses =
layout === "mobile" ? "w-full justify-center shimmer" : "px-4 rounded-xl shimmer";
return connected
? `${layoutClasses} disabled:opacity-100 disabled:cursor-default`
: layoutClasses;
}, [connected, layout]);
const zerodhaLoginMutation = useMutation({
mutationFn: async () => {
if (!apiKey.trim()) {
throw new Error("API key is required");
if (!zerodhaApiKey.trim()) {
throw new Error("Zerodha API key is required");
}
if (!apiSecret.trim()) {
throw new Error("API secret is required");
if (!zerodhaApiSecret.trim()) {
throw new Error("Zerodha API secret is required");
}
const redirectUrl = `${window.location.origin}/login`;
const res = await apiRequest("POST", "/broker/zerodha/login", {
apiKey,
apiSecret,
apiKey: zerodhaApiKey,
apiSecret: zerodhaApiSecret,
redirectUrl,
});
return res.json() as Promise<{ loginUrl: string }>;
return (await res.json()) as { loginUrl: string };
},
onSuccess: ({ loginUrl }) => {
window.open(loginUrl, "_blank", "noopener,noreferrer");
@ -103,15 +132,18 @@ export default function BrokerConnectDialog({
});
},
onError: (err: any) =>
toast({ title: "Could not start Zerodha login", description: err?.message || "Try again." }),
toast({
title: "Could not start Zerodha login",
description: err?.message || "Try again.",
}),
});
const reconnectSavedMutation = useMutation({
const zerodhaReconnectMutation = useMutation({
mutationFn: async () => {
const redirectUrl = buildReconnectRedirectUrl();
const params = new URLSearchParams({ redirectUrl });
const res = await apiRequest("GET", `/broker/login-url?${params.toString()}`);
return res.json() as Promise<{ loginUrl: string }>;
return (await res.json()) as { loginUrl: string };
},
onSuccess: ({ loginUrl }) => {
window.open(loginUrl, "_blank", "noopener,noreferrer");
@ -136,30 +168,107 @@ export default function BrokerConnectDialog({
},
});
const growwConnectMutation = useMutation({
mutationFn: async () => {
if (!growwApiKey.trim()) {
throw new Error("Groww API key is required");
}
if (!growwApiSecret.trim()) {
throw new Error("Groww API secret is required");
}
const res = await apiRequest("POST", "/broker/groww/connect", {
apiKey: growwApiKey,
apiSecret: growwApiSecret,
});
return (await res.json()) as {
connected?: boolean;
broker?: string;
userName?: string;
};
},
onSuccess: async (data) => {
await refetchStatus();
setConnectOpen(false);
toast({
title: "Groww connected",
description:
data?.userName
? `Connected as ${data.userName}.`
: "Your Groww broker session is now active.",
});
},
onError: (err: any) =>
toast({
title: "Could not connect Groww",
description: err?.message || "Try again.",
}),
});
const growwReconnectMutation = useMutation({
mutationFn: async () => {
const res = await apiRequest("POST", "/broker/groww/reconnect");
return (await res.json()) as {
connected?: boolean;
broker?: string;
userName?: string;
};
},
onSuccess: async (data) => {
await refetchStatus();
toast({
title: "Groww reconnected",
description:
data?.userName
? `Session refreshed for ${data.userName}.`
: "Your Groww broker session has been refreshed.",
});
},
onError: (err: any) => {
const message = String(err?.message || "");
if (message.includes("400:") && message.includes("Broker credentials not configured")) {
toast({
title: "Enter Groww API credentials",
description: "Saved credentials are missing. Enter the API key and secret once to reconnect.",
});
return;
}
toast({
title: "Could not reconnect Groww",
description: err?.message || "Try again.",
});
},
});
const holdingsMutation = useMutation({
mutationFn: async () => {
const res = await apiRequest("GET", "/zerodha/holdings");
return res.json() as Promise<{ holdings: any[] }>;
const res = await apiRequest("GET", "/broker/holdings");
return (await res.json()) as HoldingsResponse;
},
onSuccess: (data) => {
setHoldings(data?.holdings || []);
toast({ title: "Holdings fetched", description: "Latest positions pulled from Zerodha." });
toast({
title: "Holdings fetched",
description: `Latest positions pulled from ${formatBrokerName(data?.broker || brokerStatus?.broker)}.`,
});
},
onError: (err: any) =>
toast({
title: "Could not fetch holdings",
description: err?.message || "Check your Zerodha session and try again.",
description: err?.message || "Check your broker session and try again.",
}),
});
const disconnectMutation = useMutation({
mutationFn: async () => {
const res = await apiRequest("POST", "/broker/disconnect");
return res.json() as Promise<{ connected: boolean }>;
return (await res.json()) as { connected: boolean };
},
onSuccess: () => {
refetchStatus();
toast({ title: "Broker disconnected", description: "Your broker has been unlinked." });
onSuccess: async () => {
await refetchStatus();
toast({
title: "Broker disconnected",
description: "Your broker has been unlinked.",
});
},
onError: (err: any) =>
toast({
@ -168,20 +277,6 @@ export default function BrokerConnectDialog({
}),
});
const connected = !!brokerStatus?.connected;
const canReconnectWithSavedZerodha =
connected && (brokerStatus?.broker || "").trim().toUpperCase() === "ZERODHA";
const connectedAt = brokerStatus?.connected_at
? new Date(brokerStatus.connected_at)
: null;
const triggerClassName = useMemo(() => {
const layoutClasses =
layout === "mobile" ? "w-full justify-center shimmer" : "px-4 rounded-xl shimmer";
return connected
? `${layoutClasses} disabled:opacity-100 disabled:cursor-default`
: layoutClasses;
}, [connected, layout]);
const handleConnectClick = async () => {
if (connected) {
return;
@ -244,7 +339,7 @@ export default function BrokerConnectDialog({
<>
<Dialog open={connectOpen} onOpenChange={setConnectOpen}>
<Button
variant={connected ? "secondary" : "secondary"}
variant="secondary"
className={[triggerClassName, triggerClassNameProp].filter(Boolean).join(" ")}
disabled={connected}
onClick={handleConnectClick}
@ -252,7 +347,7 @@ export default function BrokerConnectDialog({
<PlugZap className="h-4 w-4" />
{connected ? "Broker connected" : "Connect broker"}
</Button>
<DialogContent className="sm:max-w-2xl border-border/70 bg-gradient-to-br from-background via-background to-muted/30">
<DialogContent className="sm:max-w-3xl border-border/70 bg-gradient-to-br from-background via-background to-muted/30">
<motion.div
className="space-y-4"
initial={{ opacity: 0, y: 12 }}
@ -275,29 +370,29 @@ export default function BrokerConnectDialog({
<div className="text-sm leading-tight">
<p className="font-medium">Secure brokerage linking</p>
<p className="text-muted-foreground">
Start in Zerodha and we will complete the connection automatically.
Connect with Zerodha through the broker login flow or use Groww with direct API approval.
</p>
</div>
<Badge variant={connected ? "secondary" : "outline"} className="ml-auto">
{connected ? "Connected" : "Not connected"}
</Badge>
</div>
{connected && (
{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 ${brokerStatus?.broker || "broker"}`}{" "}
-{" "}
{connectedAt
? connectedAt.toLocaleString()
: "just now"}
: `Linked to ${brokerLabel}`}{" "}
via {brokerLabel}
{" - "}
{connectedAt ? connectedAt.toLocaleString() : "just now"}
</span>
</div>
)}
) : 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={{
@ -310,7 +405,7 @@ export default function BrokerConnectDialog({
<div>
<p className="text-sm font-semibold">Zerodha</p>
<p className="text-xs text-muted-foreground">
Provide your Kite API key & secret to launch the login and connect your account.
Provide your Kite API key and secret to launch the login and connect your account.
</p>
</div>
<Badge variant="secondary">Live</Badge>
@ -322,8 +417,8 @@ export default function BrokerConnectDialog({
<Input
id="zerodha-api-key"
placeholder="Enter Zerodha API key"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
value={zerodhaApiKey}
onChange={(e) => setZerodhaApiKey(e.target.value)}
/>
</div>
<div className="space-y-1.5">
@ -332,8 +427,8 @@ export default function BrokerConnectDialog({
id="zerodha-api-secret"
placeholder="Enter Zerodha API secret"
type="password"
value={apiSecret}
onChange={(e) => setApiSecret(e.target.value)}
value={zerodhaApiSecret}
onChange={(e) => setZerodhaApiSecret(e.target.value)}
/>
</div>
</div>
@ -341,39 +436,20 @@ export default function BrokerConnectDialog({
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
onClick={() => loginUrlMutation.mutate()}
disabled={loginUrlMutation.isPending}
onClick={() => zerodhaLoginMutation.mutate()}
disabled={zerodhaLoginMutation.isPending}
>
<ArrowUpRight className="h-4 w-4" />
{loginUrlMutation.isPending ? "Opening Zerodha..." : "Open Zerodha login"}
{zerodhaLoginMutation.isPending ? "Opening Zerodha..." : "Open Zerodha login"}
</Button>
{canReconnectWithSavedZerodha ? (
<Button
variant="secondary"
onClick={() => reconnectSavedMutation.mutate()}
disabled={reconnectSavedMutation.isPending}
onClick={() => zerodhaReconnectMutation.mutate()}
disabled={zerodhaReconnectMutation.isPending}
>
<RefreshCcw className="h-4 w-4" />
{reconnectSavedMutation.isPending ? "Opening Zerodha..." : "Reconnect saved Zerodha"}
</Button>
) : null}
{connected ? (
<Button
variant="secondary"
onClick={() => holdingsMutation.mutate()}
disabled={holdingsMutation.isPending}
>
<RefreshCcw className="h-4 w-4" />
{holdingsMutation.isPending ? "Fetching..." : "Fetch holdings"}
</Button>
) : null}
{connected ? (
<Button
variant="outline"
onClick={() => disconnectMutation.mutate()}
disabled={disconnectMutation.isPending}
>
{disconnectMutation.isPending ? "Disconnecting..." : "Disconnect broker"}
{zerodhaReconnectMutation.isPending ? "Opening Zerodha..." : "Reconnect saved Zerodha"}
</Button>
) : null}
</div>
@ -388,22 +464,107 @@ export default function BrokerConnectDialog({
</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>
</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">Groww (coming soon)</Badge>
<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>
{connected && (
{connected ? (
<div className="space-y-2 rounded-lg 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" />
<p className="text-sm font-semibold">Latest holdings</p>
</div>
<div className="ml-auto flex flex-wrap gap-2">
<Button
variant="secondary"
onClick={() => holdingsMutation.mutate()}
disabled={holdingsMutation.isPending}
>
<RefreshCcw className="h-4 w-4" />
{holdingsMutation.isPending ? "Fetching..." : "Fetch holdings"}
</Button>
<Button
variant="outline"
onClick={() => disconnectMutation.mutate()}
disabled={disconnectMutation.isPending}
>
{disconnectMutation.isPending ? "Disconnecting..." : "Disconnect broker"}
</Button>
</div>
</div>
{holdings.length === 0 ? (
<p className="text-xs text-muted-foreground">
No holdings pulled yet. Click &ldquo;Fetch holdings&rdquo; after connecting.
@ -412,7 +573,7 @@ export default function BrokerConnectDialog({
<div className="grid gap-2">
{holdings.map((item, idx) => (
<div
key={`${item.tradingsymbol || item.instrument_token || idx}`}
key={`${item.tradingsymbol || item.instrument_token || item.symbol || idx}`}
className="flex items-center justify-between rounded-md border border-border/50 bg-card/40 px-3 py-2 text-sm"
>
<div>
@ -432,7 +593,7 @@ export default function BrokerConnectDialog({
</div>
)}
</div>
)}
) : null}
</div>
</motion.div>
</DialogContent>

View File

@ -35,7 +35,7 @@ const faqItems = [
{
question: "What brokers are supported?",
answer:
"Zerodha is supported now. Additional brokers will be available soon.",
"Zerodha and Groww are supported now. Additional brokers will be available soon.",
},
{
question: "Is my data secure?",

View File

@ -154,6 +154,13 @@ function buildReconnectRedirectUrl() {
return url.toString();
}
function formatBrokerName(broker?: string | null) {
const normalized = (broker || "").trim().toUpperCase();
if (normalized === "GROWW") return "Groww";
if (normalized === "ZERODHA") return "Zerodha";
return normalized || "broker";
}
function firstNumber(...values: unknown[]) {
for (const value of values) {
const parsed = Number(value);
@ -263,7 +270,7 @@ export default function PortfolioSection() {
refetchOnMount: "always",
});
async function startSavedBrokerReconnect() {
async function startSavedZerodhaReconnect() {
try {
const redirectUrl = buildReconnectRedirectUrl();
const params = new URLSearchParams({ redirectUrl });
@ -295,6 +302,36 @@ export default function PortfolioSection() {
}
}
async function startSavedGrowwReconnect() {
try {
const res = await apiRequest("POST", "/broker/groww/reconnect");
await res.json();
await Promise.allSettled([
refetchBrokerStatus(),
refreshBrokerData({ includeEquityCurve: true }),
]);
toast({
title: "Groww reconnected",
description: "Your Groww broker session has been refreshed.",
});
} catch (error: any) {
const message = String(error?.message || "");
if (message.includes("400:") && message.includes("Broker credentials not configured")) {
setBrokerDialogOpen(true);
toast({
title: "Enter Groww API credentials",
description: "Saved Groww credentials are missing. Enter the API key and secret once to reconnect.",
});
return;
}
if (message.includes("401:")) {
setLoginPromptOpen(true);
return;
}
throw error;
}
}
const disconnectBrokerMutation = useMutation({
mutationFn: async () => {
const res = await apiRequest("POST", "/broker/disconnect");
@ -306,9 +343,9 @@ export default function PortfolioSection() {
setCachedHoldings([]);
setCachedFunds(null);
setCachedEquityCurve(null);
queryClient.removeQueries({ queryKey: ["/zerodha/holdings"] });
queryClient.removeQueries({ queryKey: ["/zerodha/funds"] });
queryClient.removeQueries({ queryKey: ["/zerodha/equity-curve"] });
queryClient.removeQueries({ queryKey: ["/broker/holdings"] });
queryClient.removeQueries({ queryKey: ["/broker/funds"] });
queryClient.removeQueries({ queryKey: ["/broker/equity-curve"] });
await refetchBrokerStatus();
toast({
title: "Broker disconnected",
@ -323,9 +360,9 @@ export default function PortfolioSection() {
});
const holdingsQuery = useQuery<HoldingsResponse>({
queryKey: ["/zerodha/holdings"],
queryKey: ["/broker/holdings"],
queryFn: async () => {
const res = await apiRequest("GET", "/zerodha/holdings");
const res = await apiRequest("GET", "/broker/holdings");
return res.json();
},
enabled: !!brokerStatus?.connected,
@ -344,9 +381,9 @@ export default function PortfolioSection() {
});
const fundsQuery = useQuery<FundsResponse>({
queryKey: ["/zerodha/funds"],
queryKey: ["/broker/funds"],
queryFn: async () => {
const res = await apiRequest("GET", "/zerodha/funds");
const res = await apiRequest("GET", "/broker/funds");
return res.json();
},
enabled: !!brokerStatus?.connected,
@ -491,11 +528,11 @@ export default function PortfolioSection() {
}, [prefersReducedMotion]);
const equityCurveQuery = useQuery<EquityCurveResponse>({
queryKey: ["/zerodha/equity-curve", startDate],
queryKey: ["/broker/equity-curve", startDate],
queryFn: async () => {
const res = await apiRequest(
"GET",
`/zerodha/equity-curve${startDate ? `?from=${startDate}` : ""}`,
`/broker/equity-curve${startDate ? `?from=${startDate}` : ""}`,
);
return res.json();
},
@ -619,6 +656,8 @@ export default function PortfolioSection() {
const showSessionExpired = sessionExpired && isConnected;
const brokerAuthExpired = (brokerStatus?.authState || "").toUpperCase() === "EXPIRED";
const showReconnectBroker = isConnected && (showSessionExpired || brokerAuthExpired);
const connectedBroker = (brokerStatus?.broker || "").trim().toUpperCase();
const brokerLabel = formatBrokerName(connectedBroker);
const normalizedStrategyStatus =
strategyStatus === "RUNNING"
@ -727,17 +766,27 @@ export default function PortfolioSection() {
return true;
}, [refetchSessionUser]);
const reconnectCurrentBroker = async () => {
if (connectedBroker === "GROWW") {
return startSavedGrowwReconnect();
}
if (connectedBroker === "ZERODHA") {
return startSavedZerodhaReconnect();
}
setBrokerDialogOpen(true);
};
const handleReconnectClick = useCallback(() => {
requireLogin()
.then((isLoggedIn) => {
if (!isLoggedIn) {
return;
}
if (brokerStatus?.connected && (brokerStatus?.broker || "").trim().toUpperCase() === "ZERODHA") {
return startSavedBrokerReconnect().catch((error: any) =>
if (brokerStatus?.connected) {
return reconnectCurrentBroker().catch((error: any) =>
toast({
title: "Reconnect failed",
description: error?.message || "Unable to start Zerodha reconnect.",
description: error?.message || `Unable to reconnect ${brokerLabel}.`,
}),
);
}
@ -746,7 +795,7 @@ export default function PortfolioSection() {
.catch(() => {
setLoginPromptOpen(true);
});
}, [brokerStatus, requireLogin]);
}, [brokerLabel, brokerStatus, reconnectCurrentBroker, requireLogin]);
const handleRefreshBrokerData = useCallback(() => {
void refreshBrokerData({ includeEquityCurve: true });
@ -767,7 +816,7 @@ export default function PortfolioSection() {
if (showSessionExpired || brokerAuthExpired) {
toast({
title: "Reconnect broker",
description: "Your Zerodha session has expired. Reconnect before starting the live strategy.",
description: `Your ${brokerLabel} session has expired. Reconnect before starting the live strategy.`,
});
handleReconnectClick();
return;
@ -786,9 +835,9 @@ export default function PortfolioSection() {
if (result?.status === "broker_auth_required") {
toast({
title: "Reconnect broker",
description: "Your Zerodha session has expired. Reconnect and try again.",
description: `Your ${brokerLabel} session has expired. Reconnect and try again.`,
});
await startSavedBrokerReconnect();
await reconnectCurrentBroker();
return;
}
if (result?.status === "already_running") {
@ -800,7 +849,7 @@ export default function PortfolioSection() {
setFreshStartRequested(false);
toast({
title: "Live strategy started",
description: "Golden Nifty is now armed for live Zerodha execution.",
description: `Golden Nifty is now armed for live ${brokerLabel} execution.`,
});
} else {
toast({
@ -839,7 +888,7 @@ export default function PortfolioSection() {
if (showSessionExpired || brokerAuthExpired) {
toast({
title: "Reconnect broker",
description: "Your Zerodha session has expired. Reconnect before resuming the live strategy.",
description: `Your ${brokerLabel} session has expired. Reconnect before resuming the live strategy.`,
});
handleReconnectClick();
return;
@ -850,9 +899,9 @@ export default function PortfolioSection() {
if (result?.status === "broker_auth_required") {
toast({
title: "Reconnect broker",
description: "Your Zerodha session has expired. Reconnect and resume again.",
description: `Your ${brokerLabel} session has expired. Reconnect and resume again.`,
});
await startSavedBrokerReconnect();
await reconnectCurrentBroker();
return;
}
if (result?.status === "already_running") {

View File

@ -20,7 +20,7 @@ const privacyCards = [
},
{
title: "Third Parties",
body: "Brokerage APIs (e.g., Zerodha) are accessed only with your consent. No unauthorized sharing with any other party.",
body: "Brokerage APIs (e.g., Zerodha and Groww) are accessed only with your consent. No unauthorized sharing with any other party.",
},
{
title: "User Control",