From bc1ea376a45f16f255d579428fe94d013336af46 Mon Sep 17 00:00:00 2001 From: Thigazhezhilan J Date: Sun, 12 Apr 2026 20:17:48 +0530 Subject: [PATCH] Preserve Zerodha callback state in frontend --- package.json | 2 +- src/pages/ZerodhaCallback.tsx | 33 +++++++------------- src/pages/zerodhaCallbackUtils.test.ts | 43 ++++++++++++++++++++++++++ src/pages/zerodhaCallbackUtils.ts | 32 +++++++++++++++++++ 4 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 src/pages/zerodhaCallbackUtils.test.ts create mode 100644 src/pages/zerodhaCallbackUtils.ts diff --git a/package.json b/package.json index 6c5f7288..89edd1ee 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "vite", "build": "vite build", "typecheck": "tsc --noEmit", - "test": "npm run typecheck && tsx --test src/pages/admin/api.test.ts", + "test": "npm run typecheck && tsx --test src/pages/admin/api.test.ts src/pages/zerodhaCallbackUtils.test.ts", "preview": "vite preview", "start": "vite --host 0.0.0.0 --port 3001" }, diff --git a/src/pages/ZerodhaCallback.tsx b/src/pages/ZerodhaCallback.tsx index 99852ba5..768f830e 100644 --- a/src/pages/ZerodhaCallback.tsx +++ b/src/pages/ZerodhaCallback.tsx @@ -3,28 +3,16 @@ import { Loader2 } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { apiRequest } from "@/lib/queryClient"; import PageEnter from "@/components/ui/page-enter"; +import { + buildZerodhaBackendCallbackUrl, + parseZerodhaCallbackParams, + type ZerodhaCallbackParams, +} from "./zerodhaCallbackUtils"; const CALLBACK_STORAGE_KEY = "zerodha:callback"; const RETURN_PATH = "/portfolio"; -type CallbackParams = { - flow: string; - status: string; - requestToken: string; - error: string; - errorDescription: string; -}; - -const parseParams = (): CallbackParams => { - const params = new URLSearchParams(window.location.search); - return { - flow: (params.get("flow") || "").trim(), - status: (params.get("status") || "").trim(), - requestToken: (params.get("request_token") || "").trim(), - error: (params.get("error") || params.get("error_type") || "").trim(), - errorDescription: (params.get("error_description") || "").trim(), - }; -}; +const parseParams = (): ZerodhaCallbackParams => parseZerodhaCallbackParams(window.location.search); export default function ZerodhaCallback() { useEffect(() => { @@ -50,9 +38,12 @@ export default function ZerodhaCallback() { return; } - const callbackPath = - params.flow === "reconnect" ? "/broker/callback" : "/broker/zerodha/callback"; - const url = `${callbackPath}?request_token=${encodeURIComponent(params.requestToken)}`; + if (!params.state) { + finalize("error", "Missing callback state in the redirect URL."); + return; + } + + const url = buildZerodhaBackendCallbackUrl(params); apiRequest("GET", url) .then(() => finalize("success")) .catch((err: any) => finalize("error", err?.message || "Unable to complete the login.")); diff --git a/src/pages/zerodhaCallbackUtils.test.ts b/src/pages/zerodhaCallbackUtils.test.ts new file mode 100644 index 00000000..035a399d --- /dev/null +++ b/src/pages/zerodhaCallbackUtils.test.ts @@ -0,0 +1,43 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + buildZerodhaBackendCallbackUrl, + parseZerodhaCallbackParams, +} from "./zerodhaCallbackUtils"; + +test("parseZerodhaCallbackParams reads state from callback query", () => { + const parsed = parseZerodhaCallbackParams( + "?request_token=req-123&state=state-456&status=success&flow=reconnect", + ); + + assert.equal(parsed.requestToken, "req-123"); + assert.equal(parsed.state, "state-456"); + assert.equal(parsed.status, "success"); + assert.equal(parsed.flow, "reconnect"); +}); + +test("buildZerodhaBackendCallbackUrl forwards request token and state", () => { + const url = buildZerodhaBackendCallbackUrl({ + flow: "connect", + status: "success", + requestToken: "req-123", + state: "state-456", + error: "", + errorDescription: "", + }); + + assert.equal(url, "/broker/zerodha/callback?request_token=req-123&state=state-456"); +}); + +test("buildZerodhaBackendCallbackUrl uses reconnect callback route", () => { + const url = buildZerodhaBackendCallbackUrl({ + flow: "reconnect", + status: "success", + requestToken: "req-123", + state: "state-456", + error: "", + errorDescription: "", + }); + + assert.equal(url, "/broker/callback?request_token=req-123&state=state-456"); +}); diff --git a/src/pages/zerodhaCallbackUtils.ts b/src/pages/zerodhaCallbackUtils.ts new file mode 100644 index 00000000..9f7e82bd --- /dev/null +++ b/src/pages/zerodhaCallbackUtils.ts @@ -0,0 +1,32 @@ +export type ZerodhaCallbackParams = { + flow: string; + status: string; + requestToken: string; + state: string; + error: string; + errorDescription: string; +}; + +export function parseZerodhaCallbackParams(search: string): ZerodhaCallbackParams { + const params = new URLSearchParams(search); + return { + flow: (params.get("flow") || "").trim(), + status: (params.get("status") || "").trim(), + requestToken: (params.get("request_token") || "").trim(), + state: (params.get("state") || "").trim(), + error: (params.get("error") || params.get("error_type") || "").trim(), + errorDescription: (params.get("error_description") || "").trim(), + }; +} + +export function buildZerodhaBackendCallbackUrl(params: ZerodhaCallbackParams): string { + const callbackPath = params.flow === "reconnect" ? "/broker/callback" : "/broker/zerodha/callback"; + const search = new URLSearchParams(); + if (params.requestToken) { + search.set("request_token", params.requestToken); + } + if (params.state) { + search.set("state", params.state); + } + return `${callbackPath}?${search.toString()}`; +}