diff --git a/scripts/lazy-route-bundle-baseline.json b/scripts/lazy-route-bundle-baseline.json
index 08b2014..624f95f 100644
--- a/scripts/lazy-route-bundle-baseline.json
+++ b/scripts/lazy-route-bundle-baseline.json
@@ -1,11 +1,11 @@
{
- "src/app/routes/ProtectedLayout.tsx": 3137,
- "src/pages/account-events/index.ts": 4468,
- "src/pages/account-operations/index.ts": 13861,
- "src/pages/account/index.ts": 22366,
- "src/pages/accounts/index.ts": 6174,
- "src/pages/login/index.ts": 669,
- "src/pages/not-found/index.ts": 850,
- "src/pages/register/index.ts": 675,
- "src/pages/settings/index.ts": 38876
+ "src/app/routes/ProtectedLayout.tsx": 3077,
+ "src/pages/account-events/index.ts": 4441,
+ "src/pages/account-operations/index.ts": 13862,
+ "src/pages/account/index.ts": 22402,
+ "src/pages/accounts/index.ts": 6179,
+ "src/pages/login/index.ts": 667,
+ "src/pages/not-found/index.ts": 882,
+ "src/pages/register/index.ts": 673,
+ "src/pages/settings/index.ts": 39008
}
diff --git a/src/app/ui/AppBreadcrumbs.test.tsx b/src/app/ui/AppBreadcrumbs.test.tsx
index efad977..e0caede 100644
--- a/src/app/ui/AppBreadcrumbs.test.tsx
+++ b/src/app/ui/AppBreadcrumbs.test.tsx
@@ -8,9 +8,6 @@ const { useBrokerageAccounts } = vi.hoisted(() => ({
}));
vi.mock("@/entities/brokerage-account", () => ({ useBrokerageAccounts }));
-vi.mock("@/features/auth", () => ({
- useAuth: () => ({ accessToken: "token" }),
-}));
function renderBreadcrumbs(
path: string,
diff --git a/src/app/ui/AppBreadcrumbs.tsx b/src/app/ui/AppBreadcrumbs.tsx
index d78cdbe..3b5e18d 100644
--- a/src/app/ui/AppBreadcrumbs.tsx
+++ b/src/app/ui/AppBreadcrumbs.tsx
@@ -1,7 +1,6 @@
import { useTranslation } from "react-i18next";
import { Link, useMatch } from "react-router";
import { useBrokerageAccounts } from "@/entities/brokerage-account";
-import { useAuth } from "@/features/auth";
type Crumb = { label: string; to?: string };
@@ -45,8 +44,7 @@ function AccountBreadcrumbs({
events: boolean;
operations: boolean;
}) {
- const { accessToken } = useAuth();
- const { data: accounts } = useBrokerageAccounts(accessToken);
+ const { data: accounts } = useBrokerageAccounts();
const { t } = useTranslation();
const accountName =
accounts?.find((account) => account.id === accountId)?.name ??
diff --git a/src/entities/broker-operation/api/useBrokerageOperations.test.tsx b/src/entities/broker-operation/api/useBrokerageOperations.test.tsx
index 6761e89..b9bd722 100644
--- a/src/entities/broker-operation/api/useBrokerageOperations.test.tsx
+++ b/src/entities/broker-operation/api/useBrokerageOperations.test.tsx
@@ -2,14 +2,22 @@ import { MantineProvider } from "@mantine/core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import type { ReactNode } from "react";
-import { describe, expect, it } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { configureAuthClient } from "@/shared/api";
import {
brokerageOperationsQueryParams,
useBrokerageOperations,
} from "./useBrokerageOperations";
describe("useBrokerageOperations", () => {
+ afterEach(() => configureAuthClient(null));
+
it("normalizes query parameters and loads a page without retries", async () => {
+ configureAuthClient({
+ accessToken: () => "token",
+ refresh: async () => true,
+ clear: vi.fn(),
+ });
expect(
brokerageOperationsQueryParams({
categories: ["tax", "income"],
@@ -24,7 +32,7 @@ describe("useBrokerageOperations", () => {
);
const { result } = renderHook(
- () => useBrokerageOperations("account-1", "token", { limit: 5 }),
+ () => useBrokerageOperations("account-1", { limit: 5 }),
{ wrapper },
);
diff --git a/src/entities/broker-operation/api/useBrokerageOperations.ts b/src/entities/broker-operation/api/useBrokerageOperations.ts
index 04f3ead..15fb9e1 100644
--- a/src/entities/broker-operation/api/useBrokerageOperations.ts
+++ b/src/entities/broker-operation/api/useBrokerageOperations.ts
@@ -1,5 +1,4 @@
-import { useQuery } from "@tanstack/react-query";
-import { apiQueryClient } from "@/shared/api";
+import { apiQueryClient, useAuthenticatedQuery } from "@/shared/api";
import type { paths } from "@/shared/api/generated/schema";
type OperationsEndpoint =
@@ -37,17 +36,16 @@ export function brokerageOperationsQueryParams(
export function useBrokerageOperations(
accountId: string | undefined,
- accessToken: string | null,
query: BrokerageOperationsQuery = {},
) {
const params = brokerageOperationsQueryParams(query);
- return useQuery({
+ return useAuthenticatedQuery({
...apiQueryClient.queryOptions(
"get",
"/api/v1/brokerage-accounts/{accountId}/operations",
{ params: { path: { accountId: accountId ?? "" }, query: params } },
),
- enabled: Boolean(accountId && accessToken),
+ enabled: Boolean(accountId),
staleTime: 30_000,
retry: false,
});
diff --git a/src/entities/brokerage-account/api/useBrokerageAccounts.test.tsx b/src/entities/brokerage-account/api/useBrokerageAccounts.test.tsx
new file mode 100644
index 0000000..9464154
--- /dev/null
+++ b/src/entities/brokerage-account/api/useBrokerageAccounts.test.tsx
@@ -0,0 +1,56 @@
+import { MantineProvider } from "@mantine/core";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { renderHook, waitFor } from "@testing-library/react";
+import { HttpResponse, http } from "msw";
+import type { ReactNode } from "react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { configureAuthClient } from "@/shared/api";
+import { server } from "@/test/server";
+import { useBrokerageAccounts } from "./useBrokerageAccounts";
+
+function createWrapper() {
+ const client = new QueryClient();
+ return ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+ );
+}
+
+describe("useBrokerageAccounts", () => {
+ afterEach(() => configureAuthClient(null));
+
+ it("loads accounts through the authenticated query seam", async () => {
+ configureAuthClient({
+ accessToken: () => "token",
+ refresh: async () => true,
+ clear: vi.fn(),
+ });
+ server.use(
+ http.get("*/api/v1/brokerage-accounts", () =>
+ HttpResponse.json([
+ {
+ id: "account-1",
+ name: "Основной",
+ type: "BROKERAGE",
+ openedOn: "2024-08-13",
+ },
+ ]),
+ ),
+ );
+
+ const { result } = renderHook(() => useBrokerageAccounts(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(result.current.data).toEqual([
+ {
+ id: "account-1",
+ name: "Основной",
+ type: "BROKERAGE",
+ openedOn: "2024-08-13",
+ },
+ ]);
+ });
+});
diff --git a/src/entities/brokerage-account/api/useBrokerageAccounts.ts b/src/entities/brokerage-account/api/useBrokerageAccounts.ts
index 118c4a1..087830f 100644
--- a/src/entities/brokerage-account/api/useBrokerageAccounts.ts
+++ b/src/entities/brokerage-account/api/useBrokerageAccounts.ts
@@ -1,5 +1,4 @@
-import { useQuery } from "@tanstack/react-query";
-import { apiQueryClient } from "@/shared/api";
+import { apiQueryClient, useAuthenticatedQuery } from "@/shared/api";
import type { paths } from "@/shared/api/generated/schema";
const brokerageAccountsQueryOptions = () =>
@@ -11,10 +10,9 @@ export const brokerageAccountsQueryKey =
export type BrokerageAccount =
paths["/api/v1/brokerage-accounts"]["get"]["responses"][200]["content"]["application/json"][number];
-export function useBrokerageAccounts(accessToken: string | null) {
- return useQuery({
+export function useBrokerageAccounts() {
+ return useAuthenticatedQuery({
...brokerageAccountsQueryOptions(),
- enabled: Boolean(accessToken),
staleTime: 60_000,
retry: false,
});
diff --git a/src/entities/brokerage-event/api/useBrokerageEvents.test.tsx b/src/entities/brokerage-event/api/useBrokerageEvents.test.tsx
new file mode 100644
index 0000000..f723ebb
--- /dev/null
+++ b/src/entities/brokerage-event/api/useBrokerageEvents.test.tsx
@@ -0,0 +1,68 @@
+import { MantineProvider } from "@mantine/core";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { renderHook, waitFor } from "@testing-library/react";
+import { HttpResponse, http } from "msw";
+import type { ReactNode } from "react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { configureAuthClient } from "@/shared/api";
+import { server } from "@/test/server";
+import { useBrokerageEvents } from "./useBrokerageEvents";
+
+function createWrapper() {
+ const client = new QueryClient();
+ return ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+ );
+}
+
+describe("useBrokerageEvents", () => {
+ afterEach(() => configureAuthClient(null));
+
+ it("loads future cash events through the authenticated query seam", async () => {
+ configureAuthClient({
+ accessToken: () => "token",
+ refresh: async () => true,
+ clear: vi.fn(),
+ });
+ server.use(
+ http.get("*/api/v1/brokerage-accounts/account-1/events", () =>
+ HttpResponse.json({
+ accountId: "account-1",
+ calculatedAt: "2026-08-14T00:00:00Z",
+ events: [
+ {
+ type: "dividend",
+ date: "2026-09-01",
+ dateKind: "declared",
+ instrumentId: "figi-1",
+ ticker: "SBER",
+ displayName: "Сбер",
+ amount: "12.34",
+ currency: "RUB",
+ },
+ ],
+ }),
+ ),
+ );
+
+ const { result } = renderHook(() => useBrokerageEvents("account-1"), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(result.current.data?.events).toEqual([
+ {
+ type: "dividend",
+ date: "2026-09-01",
+ dateKind: "declared",
+ instrumentId: "figi-1",
+ ticker: "SBER",
+ displayName: "Сбер",
+ amount: "12.34",
+ currency: "RUB",
+ },
+ ]);
+ });
+});
diff --git a/src/entities/brokerage-event/api/useBrokerageEvents.ts b/src/entities/brokerage-event/api/useBrokerageEvents.ts
index 4d214b3..1faac89 100644
--- a/src/entities/brokerage-event/api/useBrokerageEvents.ts
+++ b/src/entities/brokerage-event/api/useBrokerageEvents.ts
@@ -1,5 +1,4 @@
-import { useQuery } from "@tanstack/react-query";
-import { apiQueryClient } from "@/shared/api";
+import { apiQueryClient, useAuthenticatedQuery } from "@/shared/api";
import type { paths } from "@/shared/api/generated/schema";
type EventsEndpoint =
@@ -7,17 +6,14 @@ type EventsEndpoint =
export type BrokerageEvent =
EventsEndpoint["responses"][200]["content"]["application/json"]["events"][number];
-export function useBrokerageEvents(
- accountId: string | undefined,
- accessToken: string | null,
-) {
- return useQuery({
+export function useBrokerageEvents(accountId: string | undefined) {
+ return useAuthenticatedQuery({
...apiQueryClient.queryOptions(
"get",
"/api/v1/brokerage-accounts/{accountId}/events",
{ params: { path: { accountId: accountId ?? "" } } },
),
- enabled: Boolean(accountId && accessToken),
+ enabled: Boolean(accountId),
staleTime: 30_000,
retry: false,
});
diff --git a/src/entities/portfolio/api/usePortfolio.test.tsx b/src/entities/portfolio/api/usePortfolio.test.tsx
index 6e4cd70..2be963a 100644
--- a/src/entities/portfolio/api/usePortfolio.test.tsx
+++ b/src/entities/portfolio/api/usePortfolio.test.tsx
@@ -2,12 +2,20 @@ import { MantineProvider } from "@mantine/core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import type { ReactNode } from "react";
-import { describe, expect, it } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { configureAuthClient } from "@/shared/api";
import { usePortfolio } from "./usePortfolio";
describe("usePortfolio", () => {
+ afterEach(() => configureAuthClient(null));
+
it("uses the account endpoint with a 30 second cache and no retry", async () => {
+ configureAuthClient({
+ accessToken: () => "token",
+ refresh: async () => true,
+ clear: vi.fn(),
+ });
const client = new QueryClient();
const wrapper = ({ children }: { children: ReactNode }) => (
@@ -15,7 +23,7 @@ describe("usePortfolio", () => {
);
- const { result } = renderHook(() => usePortfolio("account-1", "token"), {
+ const { result } = renderHook(() => usePortfolio("account-1"), {
wrapper,
});
diff --git a/src/entities/portfolio/api/usePortfolio.ts b/src/entities/portfolio/api/usePortfolio.ts
index d883346..5e58534 100644
--- a/src/entities/portfolio/api/usePortfolio.ts
+++ b/src/entities/portfolio/api/usePortfolio.ts
@@ -1,5 +1,4 @@
-import { useQuery } from "@tanstack/react-query";
-import { apiQueryClient } from "@/shared/api";
+import { apiQueryClient, useAuthenticatedQuery } from "@/shared/api";
import type { paths } from "@/shared/api/generated/schema";
const portfolioQueryPolicy = {
@@ -10,17 +9,14 @@ const portfolioQueryPolicy = {
export type Portfolio =
paths["/api/v1/brokerage-accounts/{accountId}/portfolio"]["get"]["responses"][200]["content"]["application/json"];
-export function usePortfolio(
- accountId: string | undefined,
- accessToken: string | null,
-) {
- return useQuery({
+export function usePortfolio(accountId: string | undefined) {
+ return useAuthenticatedQuery({
...apiQueryClient.queryOptions(
"get",
"/api/v1/brokerage-accounts/{accountId}/portfolio",
{ params: { path: { accountId: accountId ?? "" } } },
),
- enabled: Boolean(accountId && accessToken),
+ enabled: Boolean(accountId),
...portfolioQueryPolicy,
});
}
diff --git a/src/pages/account-events/ui/AccountEventsPage.test.tsx b/src/pages/account-events/ui/AccountEventsPage.test.tsx
index efe7532..5147f28 100644
--- a/src/pages/account-events/ui/AccountEventsPage.test.tsx
+++ b/src/pages/account-events/ui/AccountEventsPage.test.tsx
@@ -8,9 +8,6 @@ import { AccountEventsPage } from "./AccountEventsPage";
vi.mock("@/entities/brokerage-event", () => ({
useBrokerageEvents: vi.fn(),
}));
-vi.mock("@/features/auth", () => ({
- useAuth: () => ({ accessToken: "token" }),
-}));
const query = vi.mocked(useBrokerageEvents);
diff --git a/src/pages/account-events/ui/AccountEventsPage.tsx b/src/pages/account-events/ui/AccountEventsPage.tsx
index 7c8e21b..3b6b5c6 100644
--- a/src/pages/account-events/ui/AccountEventsPage.tsx
+++ b/src/pages/account-events/ui/AccountEventsPage.tsx
@@ -6,13 +6,11 @@ import {
useBrokerageEvents,
} from "@/entities/brokerage-event";
import { formatMoney } from "@/entities/portfolio";
-import { useAuth } from "@/features/auth";
import { StateView } from "@/shared/ui";
export function AccountEventsPage() {
const { accountId } = useParams();
- const { accessToken } = useAuth();
- const events = useBrokerageEvents(accountId, accessToken);
+ const events = useBrokerageEvents(accountId);
if (events.isLoading) return ;
if (events.isError)
return events.refetch()} />;
diff --git a/src/pages/account-operations/ui/AccountOperationsPage.test.tsx b/src/pages/account-operations/ui/AccountOperationsPage.test.tsx
index a8a3074..238c579 100644
--- a/src/pages/account-operations/ui/AccountOperationsPage.test.tsx
+++ b/src/pages/account-operations/ui/AccountOperationsPage.test.tsx
@@ -3,15 +3,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { HttpResponse, http } from "msw";
import { MemoryRouter, Route, Routes } from "react-router";
-import { describe, expect, it, vi } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
import type { BrokerageOperation } from "@/entities/broker-operation";
+import { configureAuthClient } from "@/shared/api";
import { server } from "@/test/server";
import { AccountOperationsPage, OperationRow } from "./AccountOperationsPage";
-vi.mock("@/features/auth", () => ({
- useAuth: () => ({ accessToken: "token" }),
-}));
-
const operation: BrokerageOperation = {
id: "operation-1",
occurredAt: "2026-08-14T10:00:00Z",
@@ -29,6 +26,14 @@ const operation: BrokerageOperation = {
commission: { currency: "RUB", value: "1" },
};
+function configureTestAuth() {
+ configureAuthClient({
+ accessToken: () => "token",
+ refresh: async () => true,
+ clear: vi.fn(),
+ });
+}
+
describe("OperationRow", () => {
it("marks cashless operations without rendering operation details", () => {
render(
@@ -49,7 +54,10 @@ describe("OperationRow", () => {
});
describe("AccountOperationsPage", () => {
+ afterEach(() => configureAuthClient(null));
+
it("resets cursor pagination when filters change", async () => {
+ configureTestAuth();
const searches: string[] = [];
server.use(
http.get(
@@ -96,6 +104,7 @@ describe("AccountOperationsPage", () => {
});
it("renders an error state", async () => {
+ configureTestAuth();
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }),
diff --git a/src/pages/account-operations/ui/AccountOperationsPage.tsx b/src/pages/account-operations/ui/AccountOperationsPage.tsx
index dd8603a..0216199 100644
--- a/src/pages/account-operations/ui/AccountOperationsPage.tsx
+++ b/src/pages/account-operations/ui/AccountOperationsPage.tsx
@@ -18,7 +18,6 @@ import {
useBrokerageOperations,
} from "@/entities/broker-operation";
import { formatMoney, formatSnapshotTime } from "@/entities/portfolio";
-import { useAuth } from "@/features/auth";
import { StateView } from "@/shared/ui";
function dateStart(value: string) {
@@ -31,14 +30,13 @@ function dateEnd(value: string) {
export function AccountOperationsPage() {
const { accountId } = useParams();
- const { accessToken } = useAuth();
const { t } = useTranslation();
const [from, setFrom] = useState("");
const [to, setTo] = useState("");
const [categories, setCategories] = useState([]);
const [cursors, setCursors] = useState([]);
const cursor = cursors.at(-1);
- const operations = useBrokerageOperations(accountId, accessToken, {
+ const operations = useBrokerageOperations(accountId, {
from: dateStart(from),
to: dateEnd(to),
...(categories.length ? { categories } : {}),
diff --git a/src/pages/account/ui/AccountPage.test.tsx b/src/pages/account/ui/AccountPage.test.tsx
index 1e23361..e87b69c 100644
--- a/src/pages/account/ui/AccountPage.test.tsx
+++ b/src/pages/account/ui/AccountPage.test.tsx
@@ -5,6 +5,7 @@ import { HttpResponse, http } from "msw";
import { MemoryRouter, Route, Routes } from "react-router";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { Portfolio } from "@/entities/portfolio";
+import { configureAuthClient } from "@/shared/api";
import { server } from "@/test/server";
import {
AccountPage,
@@ -19,10 +20,6 @@ import type {
} from "./AccountPage.vm";
import { toAccountPagePositionSections } from "./AccountPage.vm";
-vi.mock("@/features/auth", () => ({
- useAuth: () => ({ accessToken: "test-token" }),
-}));
-
function renderPage(ui: React.ReactNode) {
return render(
@@ -33,7 +30,16 @@ function renderPage(ui: React.ReactNode) {
);
}
+function configureTestAuth() {
+ configureAuthClient({
+ accessToken: () => "test-token",
+ refresh: async () => true,
+ clear: vi.fn(),
+ });
+}
+
function renderAccountRoute(queryClient: QueryClient) {
+ configureTestAuth();
return render(
@@ -105,7 +111,10 @@ function emptyProps() {
}
describe("AccountPage states", () => {
- afterEach(cleanup);
+ afterEach(() => {
+ configureAuthClient(null);
+ cleanup();
+ });
it("renders loading and empty states", () => {
renderPage();
expect(screen.getByRole("status")).toBeInTheDocument();
diff --git a/src/pages/account/ui/AccountPage.vm.test.tsx b/src/pages/account/ui/AccountPage.vm.test.tsx
index 9693c76..919dd16 100644
--- a/src/pages/account/ui/AccountPage.vm.test.tsx
+++ b/src/pages/account/ui/AccountPage.vm.test.tsx
@@ -4,15 +4,12 @@ import { renderHook, waitFor } from "@testing-library/react";
import { HttpResponse, http } from "msw";
import type { ReactNode } from "react";
import { MemoryRouter, Route, Routes } from "react-router";
-import { describe, expect, it, vi } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
import type { Portfolio } from "@/entities/portfolio";
+import { configureAuthClient } from "@/shared/api";
import { server } from "@/test/server";
import { useAccountPageViewModel } from "./AccountPage.vm";
-vi.mock("@/features/auth", () => ({
- useAuth: () => ({ accessToken: "test-token" }),
-}));
-
const portfolio: Portfolio = {
lastSuccessfulAt: "2026-08-13T10:00:00Z",
isStale: true,
@@ -55,6 +52,14 @@ const portfolio: Portfolio = {
cashBalances: [{ currency: "USD", value: "1" }],
};
+function configureTestAuth() {
+ configureAuthClient({
+ accessToken: () => "test-token",
+ refresh: async () => true,
+ clear: vi.fn(),
+ });
+}
+
function wrapper({ children }: { children: ReactNode }) {
return (
@@ -121,7 +126,10 @@ function mockLoadedAccountPage() {
}
describe("useAccountPageViewModel", () => {
+ afterEach(() => configureAuthClient(null));
+
it("prepares grouped position sections with derived display data", async () => {
+ configureTestAuth();
mockLoadedAccountPage();
const { result } = renderHook(() => useAccountPageViewModel(), { wrapper });
@@ -175,6 +183,7 @@ describe("useAccountPageViewModel", () => {
});
it("prepares recent operations and events state for presentation views", async () => {
+ configureTestAuth();
mockLoadedAccountPage();
const { result } = renderHook(() => useAccountPageViewModel(), { wrapper });
@@ -207,6 +216,7 @@ describe("useAccountPageViewModel", () => {
});
it("keeps recent errors isolated in the ViewModel presentation state", async () => {
+ configureTestAuth();
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
HttpResponse.json(portfolio),
diff --git a/src/pages/account/ui/AccountPage.vm.ts b/src/pages/account/ui/AccountPage.vm.ts
index fff597d..313dd15 100644
--- a/src/pages/account/ui/AccountPage.vm.ts
+++ b/src/pages/account/ui/AccountPage.vm.ts
@@ -18,7 +18,6 @@ import {
portfolioErrorCode,
usePortfolio,
} from "@/entities/portfolio";
-import { useAuth } from "@/features/auth";
type PositionSectionKind = "stocks" | "bonds" | "other";
type AccountPageReturnTone = "positive" | "negative";
@@ -86,12 +85,11 @@ export type AccountPageViewModel =
export function useAccountPageViewModel(): AccountPageViewModel {
const { accountId } = useParams();
- const { accessToken } = useAuth();
- const portfolio = usePortfolio(accountId, accessToken);
- const operations = useBrokerageOperations(accountId, accessToken, {
+ const portfolio = usePortfolio(accountId);
+ const operations = useBrokerageOperations(accountId, {
limit: 5,
});
- const events = useBrokerageEvents(accountId, accessToken);
+ const events = useBrokerageEvents(accountId);
const account = useQueryClient()
.getQueryData(brokerageAccountsQueryKey)
?.find((item) => item.id === accountId);
diff --git a/src/pages/accounts/ui/AccountsPage.test.tsx b/src/pages/accounts/ui/AccountsPage.test.tsx
index 40644f3..a76525d 100644
--- a/src/pages/accounts/ui/AccountsPage.test.tsx
+++ b/src/pages/accounts/ui/AccountsPage.test.tsx
@@ -5,13 +5,10 @@ import { HttpResponse, http } from "msw";
import { MemoryRouter } from "react-router";
import { afterEach, describe, expect, it, vi } from "vitest";
import { brokerageAccountsQueryKey } from "@/entities/brokerage-account";
+import { configureAuthClient } from "@/shared/api";
import { server } from "@/test/server";
import { AccountsPage } from "./AccountsPage";
-vi.mock("@/features/auth", () => ({
- useAuth: () => ({ accessToken: "test-token" }),
-}));
-
const account = {
id: "account-1",
name: "Основной",
@@ -19,7 +16,16 @@ const account = {
openedOn: "2026-01-01",
} as const;
+function configureTestAuth() {
+ configureAuthClient({
+ accessToken: () => "test-token",
+ refresh: async () => true,
+ clear: vi.fn(),
+ });
+}
+
function renderPage(queryClient: QueryClient) {
+ configureTestAuth();
return render(
@@ -32,7 +38,10 @@ function renderPage(queryClient: QueryClient) {
}
describe("AccountsPage", () => {
- afterEach(cleanup);
+ afterEach(() => {
+ configureAuthClient(null);
+ cleanup();
+ });
it("keeps the cached account list visible during a background refresh", async () => {
let requestCount = 0;
diff --git a/src/pages/accounts/ui/AccountsPage.vm.ts b/src/pages/accounts/ui/AccountsPage.vm.ts
index d1ac50c..71fb5f0 100644
--- a/src/pages/accounts/ui/AccountsPage.vm.ts
+++ b/src/pages/accounts/ui/AccountsPage.vm.ts
@@ -1,9 +1,7 @@
import { errorCode, useBrokerageAccounts } from "@/entities/brokerage-account";
-import { useAuth } from "@/features/auth";
export function useAccountsPageViewModel() {
- const { accessToken } = useAuth();
- const accounts = useBrokerageAccounts(accessToken);
+ const accounts = useBrokerageAccounts();
if (!accounts.data) {
if (accounts.isFetching) return { state: "loading" as const };
if (accounts.isError)
diff --git a/src/shared/api/authenticated-query.test.tsx b/src/shared/api/authenticated-query.test.tsx
new file mode 100644
index 0000000..ed2a477
--- /dev/null
+++ b/src/shared/api/authenticated-query.test.tsx
@@ -0,0 +1,68 @@
+import { MantineProvider } from "@mantine/core";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { renderHook, waitFor } from "@testing-library/react";
+import { HttpResponse, http } from "msw";
+import type { ReactNode } from "react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { server } from "@/test/server";
+import { useAuthenticatedQuery } from "./authenticated-query";
+import { apiQueryClient, configureAuthClient } from "./client";
+
+function createWrapper() {
+ const client = new QueryClient();
+ return ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+ );
+}
+
+describe("useAuthenticatedQuery", () => {
+ afterEach(() => configureAuthClient(null));
+
+ it("keeps protected queries idle without a session and loads after session configuration", async () => {
+ const requests = vi.fn();
+ server.use(
+ http.get("*/api/v1/brokerage-accounts", () => {
+ requests();
+ return HttpResponse.json([
+ {
+ id: "account-1",
+ name: "Основной",
+ type: "BROKERAGE",
+ openedOn: "2024-08-13",
+ },
+ ]);
+ }),
+ );
+
+ const { result } = renderHook(
+ () =>
+ useAuthenticatedQuery(
+ apiQueryClient.queryOptions("get", "/api/v1/brokerage-accounts"),
+ ),
+ { wrapper: createWrapper() },
+ );
+
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ expect(requests).not.toHaveBeenCalled();
+ expect(result.current.fetchStatus).toBe("idle");
+
+ configureAuthClient({
+ accessToken: () => "token",
+ refresh: async () => true,
+ clear: vi.fn(),
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(requests).toHaveBeenCalledOnce();
+ expect(result.current.data).toEqual([
+ {
+ id: "account-1",
+ name: "Основной",
+ type: "BROKERAGE",
+ openedOn: "2024-08-13",
+ },
+ ]);
+ });
+});
diff --git a/src/shared/api/authenticated-query.ts b/src/shared/api/authenticated-query.ts
new file mode 100644
index 0000000..ce63bf3
--- /dev/null
+++ b/src/shared/api/authenticated-query.ts
@@ -0,0 +1,34 @@
+import {
+ type QueryKey,
+ type UseQueryOptions,
+ type UseQueryResult,
+ useQuery,
+} from "@tanstack/react-query";
+import { useSyncExternalStore } from "react";
+import {
+ getAuthenticatedSessionSnapshot,
+ subscribeAuthenticatedSession,
+} from "./client";
+
+export function useAuthenticatedQuery<
+ TQueryFnData = unknown,
+ TError = Error,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+>(
+ options: UseQueryOptions,
+): UseQueryResult {
+ const hasSession = useSyncExternalStore(
+ subscribeAuthenticatedSession,
+ getAuthenticatedSessionSnapshot,
+ getAuthenticatedSessionSnapshot,
+ );
+ const enabled = options.enabled;
+
+ return useQuery({
+ ...options,
+ enabled: (query) =>
+ hasSession &&
+ (typeof enabled === "function" ? enabled(query) : (enabled ?? true)),
+ });
+}
diff --git a/src/shared/api/client.ts b/src/shared/api/client.ts
index 88df76e..4556aff 100644
--- a/src/shared/api/client.ts
+++ b/src/shared/api/client.ts
@@ -12,9 +12,27 @@ type AuthHandlers = {
};
let auth: AuthHandlers | null = null;
+let hasAuthenticatedSession = false;
+const authSessionListeners = new Set<() => void>();
+
+function setAuthenticatedSession(next: boolean) {
+ if (next === hasAuthenticatedSession) return;
+ hasAuthenticatedSession = next;
+ for (const listener of authSessionListeners) listener();
+}
export function configureAuthClient(handlers: AuthHandlers | null) {
auth = handlers;
+ setAuthenticatedSession(Boolean(auth?.accessToken()));
+}
+
+export function getAuthenticatedSessionSnapshot() {
+ return hasAuthenticatedSession;
+}
+
+export function subscribeAuthenticatedSession(listener: () => void) {
+ authSessionListeners.add(listener);
+ return () => authSessionListeners.delete(listener);
}
async function authenticatedFetch(request: Request): Promise {
diff --git a/src/shared/api/index.ts b/src/shared/api/index.ts
index 0a90b21..32ad258 100644
--- a/src/shared/api/index.ts
+++ b/src/shared/api/index.ts
@@ -1 +1,2 @@
+export { useAuthenticatedQuery } from "./authenticated-query";
export { apiClient, apiQueryClient, configureAuthClient } from "./client";