From 32e21de17d00de5b3a4b074a2300e6a29a81e3ae Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 13 Aug 2026 19:31:43 +0300 Subject: [PATCH] feat: add tbank tokens --- src/features/tbank-connection/index.ts | 1 + .../ui/TbankConnectionSection.tsx | 167 ++++++++++++++++++ src/pages/settings/ui/SettingsPage.tsx | 2 + src/shared/api/generated/schema.d.ts | 83 +++++++++ src/shared/i18n/locales/ru.ts | 20 +++ 5 files changed, 273 insertions(+) create mode 100644 src/features/tbank-connection/index.ts create mode 100644 src/features/tbank-connection/ui/TbankConnectionSection.tsx diff --git a/src/features/tbank-connection/index.ts b/src/features/tbank-connection/index.ts new file mode 100644 index 0000000..c128eb1 --- /dev/null +++ b/src/features/tbank-connection/index.ts @@ -0,0 +1 @@ +export { TbankConnectionSection } from "./ui/TbankConnectionSection"; diff --git a/src/features/tbank-connection/ui/TbankConnectionSection.tsx b/src/features/tbank-connection/ui/TbankConnectionSection.tsx new file mode 100644 index 0000000..8b4e8d7 --- /dev/null +++ b/src/features/tbank-connection/ui/TbankConnectionSection.tsx @@ -0,0 +1,167 @@ +import { + Alert, + Button, + Modal, + PasswordInput, + Stack, + Text, +} from "@mantine/core"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { apiClient } from "../../../shared/api"; +import type { paths } from "../../../shared/api/generated/schema"; +import { useAuth } from "../../auth"; + +type Status = + paths["/api/v1/tbank-connection"]["get"]["responses"][200]["content"]["application/json"]; + +function errorCode(error: unknown): string | undefined { + return typeof error === "object" && error !== null && "code" in error + ? String(error.code) + : undefined; +} + +export function TbankConnectionSection() { + const { t } = useTranslation(); + const { accessToken } = useAuth(); + const queryClient = useQueryClient(); + const [deleteOpened, setDeleteOpened] = useState(false); + const [requestError, setRequestError] = useState(null); + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm<{ token: string }>(); + const headers = { Authorization: `Bearer ${accessToken}` }; + const status = useQuery({ + queryKey: ["tbank-connection"], + queryFn: async () => { + const { data, error } = await apiClient.GET("/api/v1/tbank-connection", { + headers, + }); + if (error) throw error; + return data as Status; + }, + }); + const update = useMutation({ + mutationFn: async ({ token }: { token: string }) => { + const { data, error } = await apiClient.PUT("/api/v1/tbank-connection", { + headers, + body: { token }, + }); + if (error) throw error; + return data as Status; + }, + onSuccess: (data) => { + queryClient.setQueryData(["tbank-connection"], data); + reset(); + }, + onError: (error) => setRequestError(errorCode(error)), + }); + const remove = useMutation({ + mutationFn: async () => { + const { error } = await apiClient.DELETE("/api/v1/tbank-connection", { + headers, + }); + if (error) throw error; + }, + onSuccess: () => { + queryClient.setQueryData(["tbank-connection"], { + connected: false, + connectedAt: null, + }); + setDeleteOpened(false); + }, + onError: () => setRequestError("REQUEST_ERROR"), + }); + const pending = update.isPending || remove.isPending; + + if (status.isPending) return {t("states.loadingText")}; + if (status.isError) + return ( + + + + ); + const connected = status.data.connected; + return ( + + {t("tbank.title")} + + {connected + ? t("tbank.connected", { + date: new Date( + String(status.data.connectedAt), + ).toLocaleDateString("ru-RU"), + }) + : t("tbank.disconnected")} + + + {t("tbank.hint")} + +
{ + setRequestError(null); + update.mutate({ token }); + })} + > +
+ + + {requestError && ( + + {t( + requestError === "INVALID_TBANK_TOKEN" + ? "tbank.invalidToken" + : requestError === "TBANK_UNAVAILABLE" + ? "tbank.unavailable" + : "tbank.requestError", + )} + + )} + + {connected && ( + + )} + +
+
+ !pending && setDeleteOpened(false)} + title={t("tbank.deleteTitle")} + closeOnClickOutside={!pending} + closeOnEscape={!pending} + > + + {t("tbank.deleteText")} + + + +
+ ); +} diff --git a/src/pages/settings/ui/SettingsPage.tsx b/src/pages/settings/ui/SettingsPage.tsx index eba184d..9348bbf 100644 --- a/src/pages/settings/ui/SettingsPage.tsx +++ b/src/pages/settings/ui/SettingsPage.tsx @@ -2,6 +2,7 @@ import { Button, Stack, Title } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; import { useAuth } from "../../../features/auth"; +import { TbankConnectionSection } from "../../../features/tbank-connection"; import { StateView } from "../../../shared/ui/StateView"; export function SettingsPage() { @@ -11,6 +12,7 @@ export function SettingsPage() { return ( {t("settings.title")} +