Frontend security / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 4m57s
Conventional commits / commits (pull_request) Successful in 28s
Frontend security / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 5m10s
Repair investment recovery, draft preservation, date validation, currency presentation, accessibility, and route error recovery. Correct Docker and dependency checks and consume explicit income roles.
173 lines
4.3 KiB
TypeScript
173 lines
4.3 KiB
TypeScript
import {
|
|
Alert,
|
|
Button,
|
|
Modal,
|
|
PasswordInput,
|
|
Stack,
|
|
Text,
|
|
} from "@mantine/core";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import type { FC } from "react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { useTranslation } from "react-i18next";
|
|
import { apiErrorCode, apiQueryClient } from "@/shared/api";
|
|
import {
|
|
commitTbankConnectionBoundary,
|
|
type TbankConnectionStatus,
|
|
} from "../../model/connection-boundary";
|
|
|
|
type TbankConnectionSectionLoadedProps = {
|
|
status: TbankConnectionStatus;
|
|
refreshing: boolean;
|
|
refreshError: boolean;
|
|
retry: () => void;
|
|
};
|
|
|
|
const TbankConnectionSectionLoaded: FC<TbankConnectionSectionLoadedProps> = (
|
|
props,
|
|
) => {
|
|
const { status } = props;
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
const [deleteOpened, setDeleteOpened] = useState(false);
|
|
const deleteButton = useRef<HTMLButtonElement>(null);
|
|
const [requestError, setRequestError] = useState<string | null>(null);
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
reset,
|
|
formState: { errors },
|
|
} = useForm<{ token: string }>();
|
|
const update = apiQueryClient.useMutation("put", "/api/v1/tbank-connection", {
|
|
onSuccess: (data) => {
|
|
commitTbankConnectionBoundary(queryClient, data);
|
|
reset();
|
|
},
|
|
onError: (error) => setRequestError(apiErrorCode(error) ?? "REQUEST_ERROR"),
|
|
});
|
|
const remove = apiQueryClient.useMutation(
|
|
"delete",
|
|
"/api/v1/tbank-connection",
|
|
{
|
|
onSuccess: () => {
|
|
commitTbankConnectionBoundary(queryClient, {
|
|
connected: false,
|
|
connectedAt: null,
|
|
});
|
|
setDeleteOpened(false);
|
|
},
|
|
},
|
|
);
|
|
const pending = update.isPending || remove.isPending;
|
|
const connected = status.connected;
|
|
useEffect(() => {
|
|
if (deleteOpened && remove.isError) deleteButton.current?.focus();
|
|
}, [deleteOpened, remove.isError]);
|
|
|
|
return (
|
|
<Stack mt="xl">
|
|
<Text fw={600}>{t("tbank.title")}</Text>
|
|
<Text size="sm">
|
|
{connected
|
|
? t("tbank.connected", {
|
|
date: new Date(status.connectedAt ?? "").toLocaleDateString(
|
|
"ru-RU",
|
|
),
|
|
})
|
|
: t("tbank.disconnected")}
|
|
</Text>
|
|
<Text size="sm" c="dimmed">
|
|
{t("tbank.hint")}
|
|
</Text>
|
|
{props.refreshing && (
|
|
<Text role="status" size="sm" c="dimmed">
|
|
{t("tbank.refreshing")}
|
|
</Text>
|
|
)}
|
|
{props.refreshError && (
|
|
<Alert color="orange" role="status" aria-live="polite">
|
|
<Stack gap="sm">
|
|
<Text>{t("tbank.refreshError")}</Text>
|
|
<Button
|
|
variant="default"
|
|
onClick={props.retry}
|
|
disabled={props.refreshing || pending}
|
|
>
|
|
{t("tbank.retry")}
|
|
</Button>
|
|
</Stack>
|
|
</Alert>
|
|
)}
|
|
<form
|
|
onSubmit={handleSubmit(({ token }) => {
|
|
setRequestError(null);
|
|
update.mutate({ body: { token } });
|
|
})}
|
|
>
|
|
<fieldset disabled={pending}>
|
|
<Stack>
|
|
<PasswordInput
|
|
label={t("tbank.token")}
|
|
{...register("token", { required: t("tbank.tokenRequired") })}
|
|
error={errors.token?.message}
|
|
/>
|
|
{requestError && (
|
|
<Alert color="red">
|
|
{t(
|
|
requestError === "INVALID_TBANK_TOKEN"
|
|
? "tbank.invalidToken"
|
|
: requestError === "TBANK_UNAVAILABLE"
|
|
? "tbank.unavailable"
|
|
: "tbank.requestError",
|
|
)}
|
|
</Alert>
|
|
)}
|
|
<Button type="submit" loading={update.isPending}>
|
|
{t(connected ? "tbank.replace" : "tbank.connect")}
|
|
</Button>
|
|
{connected && (
|
|
<Button
|
|
variant="outline"
|
|
color="red"
|
|
onClick={() => {
|
|
remove.reset();
|
|
setDeleteOpened(true);
|
|
}}
|
|
>
|
|
{t("tbank.delete")}
|
|
</Button>
|
|
)}
|
|
</Stack>
|
|
</fieldset>
|
|
</form>
|
|
<Modal
|
|
opened={deleteOpened}
|
|
onClose={() => !pending && setDeleteOpened(false)}
|
|
title={t("tbank.deleteTitle")}
|
|
closeOnClickOutside={!pending}
|
|
closeOnEscape={!pending}
|
|
closeButtonProps={{ "aria-label": t("tbank.close"), disabled: pending }}
|
|
>
|
|
<Stack>
|
|
<Text>{t("tbank.deleteText")}</Text>
|
|
{remove.isError && (
|
|
<Alert color="red">{t("tbank.requestError")}</Alert>
|
|
)}
|
|
<Button
|
|
ref={deleteButton}
|
|
color="red"
|
|
loading={remove.isPending}
|
|
disabled={pending}
|
|
onClick={() => remove.mutate({})}
|
|
>
|
|
{t("tbank.deleteConfirm")}
|
|
</Button>
|
|
</Stack>
|
|
</Modal>
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
export { TbankConnectionSectionLoaded };
|