diff --git a/package.json b/package.json index d15dabb..14b55cd 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dependencies": { "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", - "@frontend/kitui": "1.0.53", + "@frontend/kitui": "1.0.54", "@mui/icons-material": "^5.10.14", "@mui/material": "^5.10.14", "@popperjs/core": "^2.11.8", @@ -29,6 +29,7 @@ "pdfjs-dist": "3.6.172", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.11", "react-pdf": "^7.1.2", "react-router-dom": "^6.15.0", "react-slick": "^0.29.0", diff --git a/src/components/Drawers.tsx b/src/components/Drawers.tsx index 0896626..66ca6e0 100644 --- a/src/components/Drawers.tsx +++ b/src/components/Drawers.tsx @@ -30,8 +30,11 @@ import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg"; import { payCart } from "@root/api/cart"; import { enqueueSnackbar } from "notistack"; import { Link, useNavigate } from "react-router-dom"; +import { withErrorBoundary } from "react-error-boundary"; +import { handleComponentError } from "@root/utils/handleComponentError"; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -export default function Drawers() { +function Drawers() { const [openNotificationsModal, setOpenNotificationsModal] = useState(false); const [loading, setLoading] = useState(false); @@ -57,12 +60,15 @@ export default function Drawers() { const [payCartResponse, payCartError] = await payCart(); if (payCartError) { - const notEnoughMoneyAmount = parseInt( - payCartError.replace("insufficient funds: ", "") - ); + if (payCartError.includes("insufficient funds: ")) { + const notEnoughMoneyAmount = parseInt( + payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, "") + ); - setNotEnoughMoneyAmount(notEnoughMoneyAmount); - setLoading(false); + setNotEnoughMoneyAmount(notEnoughMoneyAmount); + } + + setLoading(false); return enqueueSnackbar(payCartError); } @@ -72,6 +78,7 @@ export default function Drawers() { } setLoading(false); + closeCartDrawer(); } function handleReplenishWallet() { @@ -303,8 +310,6 @@ export default function Drawers() { + + + ); +} + +export default withErrorBoundary(AccountSettings, { + fallback: Ошибка при отображении настроек аккаунта, + onError: handleComponentError, +}) + +const verificationStatusData: Record = { verificated: { text: "Верификация пройдена", color: "#0D9F00" }, waiting: { text: "В ожидании верификации", color: "#F18956" }, notVerificated: { text: "Не верифицирован", color: "#E02C2C" }, - }; +}; - function handleSendDataClick() { - sendUserData() - .then(() => { - enqueueSnackbar("Информация обновлена"); - }) - .catch((error) => { - const message = getMessageFromFetchError(error); - if (message) enqueueSnackbar(message); - }); - } - - function VerificationIndicator({ +function VerificationIndicator({ verificationStatus, sx, - }: { +}: { verificationStatus: VerificationStatus; sx?: SxProps; - }) { +}) { return ( - - {verificationStatusData[verificationStatus].text} - - ); - } - - console.log("компонент настройки юзера") - return ( - - - - Настройки аккаунта - - - - - Статус - - {verificationStatus === VerificationStatus.NOT_VERIFICATED && ( - <> - } - sx={{ mt: "55px" }} - ButtonProps={{ - onClick: () => openDocumentsDialog("juridical"), - }} - > - Загрузить документы для юр лиц - - } - sx={{ mt: "15px" }} - ButtonProps={{ - onClick: () => openDocumentsDialog("nko"), - }} - > - Загрузить документы для НКО - - - )} - {verificationStatus === VerificationStatus.VERIFICATED && ( - } - sx={{ mt: "55px" }} - ButtonProps={{ - onClick: () => openDocumentsDialog(verificationType), - }} - > - Посмотреть свою верификацию - - )} - {comment &&

{comment}

} -
-
- -
-
- ); -} + {verificationStatusData[verificationStatus].text} + + ); +} \ No newline at end of file diff --git a/src/pages/Cart/Cart.tsx b/src/pages/Cart/Cart.tsx index 84216c3..fb744bb 100644 --- a/src/pages/Cart/Cart.tsx +++ b/src/pages/Cart/Cart.tsx @@ -6,8 +6,10 @@ import CustomWrapper from "./CustomWrapper"; import { useCart } from "@root/utils/hooks/useCart"; import { useLocation } from "react-router-dom"; import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation"; +import { handleComponentError } from "@root/utils/handleComponentError"; +import { withErrorBoundary } from "react-error-boundary"; -export default function Cart() { +function Cart() { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const isMobile = useMediaQuery(theme.breakpoints.down(550)); @@ -71,3 +73,8 @@ export default function Cart() { ); } + +export default withErrorBoundary(Cart, { + fallback: Ошибка при отображении корзины, + onError: handleComponentError, +}) diff --git a/src/pages/History/index.tsx b/src/pages/History/index.tsx index 16ca86a..2f9f428 100644 --- a/src/pages/History/index.tsx +++ b/src/pages/History/index.tsx @@ -11,6 +11,8 @@ import { HISTORY } from "./historyMocks"; import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"; import { useHistoryData } from "@root/utils/hooks/useHistoryData"; import { isArray } from "cypress/types/lodash"; +import { ErrorBoundary } from "react-error-boundary"; +import { handleComponentError } from "@root/utils/handleComponentError"; const subPages = ["Платежи", "Покупки тарифов", "Окончания тарифов"]; @@ -67,23 +69,30 @@ export default function History() { ) : ( )} - {historyData?.records - .filter((e) => { - e.createdAt = extractDateFromString(e.createdAt) - return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value) - )}) - .map(( e, index) => { - return ( - - - - )})} + Ошибка загрузки истории + } + onError={handleComponentError} + > + {historyData?.records + .filter((e) => { + e.createdAt = extractDateFromString(e.createdAt) + return(!e.isDeleted && e.key === "payCart" && Array.isArray(e.rawDetails[0].Value) + )}) + .map(( e, index) => { + return ( + + + + )})} + ); } diff --git a/src/pages/SavedTariffs/index.tsx b/src/pages/SavedTariffs/index.tsx index 4b37a2b..fada753 100644 --- a/src/pages/SavedTariffs/index.tsx +++ b/src/pages/SavedTariffs/index.tsx @@ -5,8 +5,10 @@ import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"; import SaveWrapper from "./SaveWrapper"; import { useTariffStore } from "@root/stores/tariffs"; import { type Tariff } from "@frontend/kitui"; +import { withErrorBoundary } from "react-error-boundary"; +import { handleComponentError } from "@root/utils/handleComponentError"; -export default function SavedTariffs() { +function SavedTariffs() { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const isMobile = useMediaQuery(theme.breakpoints.down(550)); @@ -65,3 +67,8 @@ export default function SavedTariffs() { ); } + +export default withErrorBoundary(SavedTariffs, { + fallback: Ошибка при отображении сохраненных тарифов, + onError: handleComponentError, +}) diff --git a/src/pages/Support/SupportChat.tsx b/src/pages/Support/SupportChat.tsx index d275a7b..88409a9 100644 --- a/src/pages/Support/SupportChat.tsx +++ b/src/pages/Support/SupportChat.tsx @@ -35,8 +35,10 @@ import { useTicketMessages, } from "@frontend/kitui"; import { shownMessage, sendTicketMessage } from "@root/api/ticket"; +import { withErrorBoundary } from "react-error-boundary"; +import { handleComponentError } from "@root/utils/handleComponentError"; -export default function SupportChat() { +function SupportChat() { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const isMobile = useMediaQuery(theme.breakpoints.up(460)); @@ -128,7 +130,7 @@ export default function SupportChat() { async function handleSendMessage() { if (!ticket || !messageField) return; - const [_, sendTicketMessageError] = await sendTicketMessage( + const [, sendTicketMessageError] = await sendTicketMessage( ticket.id, messageField ); @@ -318,3 +320,8 @@ export default function SupportChat() { ); } + +export default withErrorBoundary(SupportChat, { + fallback: Не удалось отобразить чат, + onError: handleComponentError, +}) diff --git a/src/pages/Support/TicketList/TicketList.tsx b/src/pages/Support/TicketList/TicketList.tsx index 22ad915..98035c3 100644 --- a/src/pages/Support/TicketList/TicketList.tsx +++ b/src/pages/Support/TicketList/TicketList.tsx @@ -5,13 +5,16 @@ import { Box, useTheme, Pagination, + Typography, } from "@mui/material"; import TicketCard from "./TicketCard"; import { setTicketApiPage, useTicketStore } from "@root/stores/tickets"; import { Ticket } from "@frontend/kitui"; +import { withErrorBoundary } from "react-error-boundary"; +import { handleComponentError } from "@root/utils/handleComponentError"; -export default function TicketList() { +function TicketList() { const theme = useTheme(); const tickets = useTicketStore((state) => state.tickets); const ticketCount = useTicketStore((state) => state.ticketCount); @@ -87,3 +90,8 @@ function sortTicketsByUpdateTime(ticket1: Ticket, ticket2: Ticket) { const date2 = new Date(ticket2.updated_at).getTime(); return date2 - date1; } + +export default withErrorBoundary(TicketList, { + fallback: Ошибка загрузки тикетов, + onError: handleComponentError, +}); diff --git a/src/pages/TariffConstructor/TariffConstructor.tsx b/src/pages/TariffConstructor/TariffConstructor.tsx index c007bbf..d36c667 100644 --- a/src/pages/TariffConstructor/TariffConstructor.tsx +++ b/src/pages/TariffConstructor/TariffConstructor.tsx @@ -1,4 +1,4 @@ -import { Box, IconButton, useMediaQuery, useTheme } from "@mui/material"; +import { Box, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Link } from "react-router-dom"; import SectionWrapper from "@components/SectionWrapper"; import { useCustomTariffsStore } from "@root/stores/customTariffs"; @@ -8,8 +8,10 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import TotalPrice from "@root/components/TotalPrice"; import { serviceNameByKey } from "@root/utils/serviceKeys"; import { useHistoryTracker } from "@root/utils/hooks/useHistoryTracker"; +import { withErrorBoundary } from "react-error-boundary"; +import { handleComponentError } from "@root/utils/handleComponentError"; -export default function TariffConstructor() { +function TariffConstructor() { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); @@ -84,3 +86,8 @@ export default function TariffConstructor() { ); } + +export default withErrorBoundary(TariffConstructor, { + fallback: Ошибка при отображении кастомных тарифов, + onError: handleComponentError, +}) diff --git a/src/pages/TariffConstructor/TariffItem.tsx b/src/pages/TariffConstructor/TariffItem.tsx index aad11e2..1368e97 100644 --- a/src/pages/TariffConstructor/TariffItem.tsx +++ b/src/pages/TariffConstructor/TariffItem.tsx @@ -28,6 +28,7 @@ export default function TariffPrivilegeSlider({ privilege }: Props) { const discounts = useDiscountStore((state) => state.discounts); const currentCartTotal = useCartStore((state) => state.cart.priceAfterDiscounts); const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.purchasesAmount) ?? 0; + const isUserNko = useUserStore(state => state.userAccount?.status) === "nko"; const [value, setValue] = useState(userValue); const throttledValue = useThrottle(value, 200); @@ -39,10 +40,11 @@ export default function TariffPrivilegeSlider({ privilege }: Props) { throttledValue, discounts, currentCartTotal, - purchasesAmount + purchasesAmount, + isUserNko, ); }, - [currentCartTotal, discounts, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue] + [currentCartTotal, discounts, purchasesAmount, privilege._id, privilege.serviceKey, throttledValue, isUserNko] ); function handleSliderChange(value: number | number[]) { diff --git a/src/pages/Tariffs/TariffsPage.tsx b/src/pages/Tariffs/TariffsPage.tsx index 6248ec0..93b6832 100644 --- a/src/pages/Tariffs/TariffsPage.tsx +++ b/src/pages/Tariffs/TariffsPage.tsx @@ -18,6 +18,8 @@ import { Slider } from "./slider"; import { useCartStore } from "@root/stores/cart"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import { usePrevLocation } from "@root/utils/hooks/handleCustomBackNavigation"; +import { withErrorBoundary } from "react-error-boundary"; +import { handleComponentError } from "@root/utils/handleComponentError"; const subPages = ["Шаблонизатор", "Опросник", "Сокращатель ссылок"]; @@ -26,7 +28,7 @@ const StepperText: Record = { time: "Тарифы на время", }; -export default function TariffPage() { +function TariffPage() { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const isMobile = useMediaQuery(theme.breakpoints.down(600)); @@ -178,3 +180,8 @@ export default function TariffPage() { ); } + +export default withErrorBoundary(TariffPage, { + fallback: Ошибка загрузки тарифов, + onError: handleComponentError, +}) diff --git a/src/stores/customTariffs.ts b/src/stores/customTariffs.ts index 45610aa..1a6ca6d 100644 --- a/src/stores/customTariffs.ts +++ b/src/stores/customTariffs.ts @@ -5,6 +5,7 @@ import { produce } from "immer"; import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; import { Discount, PrivilegeWithAmount, findCartDiscount, findDiscountFactor, findLoyaltyDiscount, findPrivilegeDiscount, findServiceDiscount } from "@frontend/kitui"; +import { findNkoDiscount } from "@root/utils/calcCart/calcCart"; interface CustomTariffsStore { @@ -50,6 +51,7 @@ export const setCustomTariffsUserValue = ( discounts: Discount[], currentCartTotal: number, purchasesAmount: number, + isUserNko: boolean, ) => useCustomTariffsStore.setState( produce(state => { state.userValuesMap[serviceKey] ??= {}; @@ -58,22 +60,33 @@ export const setCustomTariffsUserValue = ( let priceWithoutDiscounts = 0; let priceAfterDiscounts = 0; - state.privilegeByService[serviceKey].forEach(privilege => { - const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0; - priceWithoutDiscounts += privilege.price * amount; + const nkoDiscount = findNkoDiscount(discounts); - const discount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * amount, discounts); - priceAfterDiscounts += privilege.price * amount * findDiscountFactor(discount); - }); + if (isUserNko && nkoDiscount) { + state.privilegeByService[serviceKey].forEach(privilege => { + const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0; + priceWithoutDiscounts += privilege.price * amount; + }); - const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts); - priceAfterDiscounts *= findDiscountFactor(serviceDiscount); + priceAfterDiscounts = priceWithoutDiscounts * findDiscountFactor(nkoDiscount); + } else { + state.privilegeByService[serviceKey].forEach(privilege => { + const amount = state.userValuesMap[serviceKey]?.[privilege._id] ?? 0; + priceWithoutDiscounts += privilege.price * amount; - const cartDiscount = findCartDiscount(currentCartTotal, discounts); - priceAfterDiscounts *= findDiscountFactor(cartDiscount); + const discount = findPrivilegeDiscount(privilege.privilegeId, privilege.price * amount, discounts); + priceAfterDiscounts += privilege.price * amount * findDiscountFactor(discount); + }); - const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts); - priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount); + const serviceDiscount = findServiceDiscount(serviceKey, priceAfterDiscounts, discounts); + priceAfterDiscounts *= findDiscountFactor(serviceDiscount); + + const cartDiscount = findCartDiscount(currentCartTotal, discounts); + priceAfterDiscounts *= findDiscountFactor(cartDiscount); + + const loyaltyDiscount = findLoyaltyDiscount(purchasesAmount, discounts); + priceAfterDiscounts *= findDiscountFactor(loyaltyDiscount); + } state.summaryPriceBeforeDiscountsMap[serviceKey] = priceWithoutDiscounts; state.summaryPriceAfterDiscountsMap[serviceKey] = priceAfterDiscounts; diff --git a/src/stores/user.ts b/src/stores/user.ts index 4858db5..c023869 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -279,7 +279,7 @@ export const sendUserData = async () => { export const addTariffToCart = async (tariffId: string) => { const [patchCartResponse, patchCartError] = await patchCart(tariffId); - if (patchCartError !== undefined) { + if (patchCartError === undefined) { setCart(patchCartResponse); } return({patchCartResponse, patchCartError}) diff --git a/src/utils/calcCart/calcCart.ts b/src/utils/calcCart/calcCart.ts index 3cf37db..92b62fe 100644 --- a/src/utils/calcCart/calcCart.ts +++ b/src/utils/calcCart/calcCart.ts @@ -75,7 +75,7 @@ function applyNkoDiscount(cartData: CartData, discount: Discount) { cartData.allAppliedDiscounts.push(discount); } -function findNkoDiscount(discounts: Discount[]): Discount | null { +export function findNkoDiscount(discounts: Discount[]): Discount | null { const applicableDiscounts = discounts.filter(discount => discount.Condition.UserType === "nko"); if (!applicableDiscounts.length) return null; diff --git a/src/utils/handleComponentError.ts b/src/utils/handleComponentError.ts new file mode 100644 index 0000000..e6ae5bd --- /dev/null +++ b/src/utils/handleComponentError.ts @@ -0,0 +1,44 @@ +import { makeRequest } from "@frontend/kitui"; +import { ErrorInfo } from "react"; + + +interface ComponentError { + timestamp: number; + message: string; + callStack: string | undefined; + componentStack: string; +} + +export function handleComponentError(error: Error, info: ErrorInfo) { + const componentError: ComponentError = { + timestamp: Math.floor(Date.now() / 1000), + message: error.message, + callStack: error.stack, + componentStack: info.componentStack, + }; + + queueErrorRequest(componentError); +} + +let errorsQueue: ComponentError[] = []; +let timeoutId: ReturnType; + +function queueErrorRequest(error: ComponentError) { + errorsQueue.push(error); + + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + sendErrorsToServer(); + }, 1000); +} + +async function sendErrorsToServer() { + // makeRequest({ + // url: "", + // method: "POST", + // body: errorsQueue, + // useToken: true, + // }); + console.log(`Fake-sending ${errorsQueue.length} errors to server`, errorsQueue); + errorsQueue = []; +} diff --git a/yarn.lock b/yarn.lock index 8217f37..0b0708c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1532,10 +1532,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@frontend/kitui@1.0.53": - version "1.0.53" - resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.53.tgz#a663052d300b9e3c588346c646f276c9aec7de5d" - integrity sha1-pmMFLTALnjxYg0bGRvJ2ya7H3l0= +"@frontend/kitui@1.0.54": + version "1.0.54" + resolved "https://penahub.gitlab.yandexcloud.net/api/v4/projects/21/packages/npm/@frontend/kitui/-/@frontend/kitui-1.0.54.tgz#0235d5a8effb9b92351471c3c7775f28cb2839f6" + integrity sha1-AjXVqO/7m5I1FHHDx3dfKMsoOfY= dependencies: immer "^10.0.2" reconnecting-eventsource "^1.6.2" @@ -9448,6 +9448,13 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-error-boundary@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c" + integrity sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz"