diff --git a/.gitea/workflows/deployProd.yml b/.gitea/workflows/deployProd.yml index c45e6fb..f7bf287 100644 --- a/.gitea/workflows/deployProd.yml +++ b/.gitea/workflows/deployProd.yml @@ -2,9 +2,8 @@ name: Deploy run-name: ${{ gitea.actor }} build image and push to container registry on: - push: - branches: - - 'main' + registry_package: + types: [published] jobs: CreateImage: diff --git a/.gitea/workflows/deployStaging.yml b/.gitea/workflows/deployStaging.yml index 3495235..97c1569 100644 --- a/.gitea/workflows/deployStaging.yml +++ b/.gitea/workflows/deployStaging.yml @@ -2,25 +2,30 @@ name: Deploy run-name: ${{ gitea.actor }} build image and push to container registry on: - push: - branches: - - 'staging' + registry_package: + types: [published] jobs: - CreateImage: - runs-on: [hubstaging] - uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p - with: - runner: hubstaging - secrets: - REGISTRY_USER: ${{ secrets.REGISTRY_USER }} - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + # CreateImage: + # runs-on: [skeris] + # uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p + # with: + # runner: skeris + # secrets: + # REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + # REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} DeployService: + if: contains(github.event.package.name, 'staging') runs-on: [frontstaging] - needs: CreateImage - uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7 - with: - runner: frontstaging - actionid: ${{ gitea.run_id }} + container: + image: gitea.pena:3000/penadevops/container-images/node-compose:main + env: + GITHUB_RUN_NUMBER: "${{ inputs.actionid }}" + volumes: + - /run/user/1000/docker/docker.sock:/run/user/1000/docker/docker.sock + steps: + - name: Check out repository code + uses: http://gitea.pena:3000/PenaDevops/actions.git/checkout@v1 + - run: compose -f deployments/staging/docker-compose.yaml up -d diff --git a/.husky/pre-commit b/.husky/pre-commit index 3e48a61..b1d2126 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,3 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" yarn eslint . --fix +yarn type-check diff --git a/Containerfile b/Dockerfile similarity index 79% rename from Containerfile rename to Dockerfile index 27cf567..6257b54 100644 --- a/Containerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM gitea.pena/penadevops/container-images/node:main as build +FROM docker.io/library/node:lts-alpine as build RUN apk update && rm -rf /var/cache/apk/* WORKDIR /usr/app COPY . . RUN npm install --force && yarn cache clean -RUN psstat.sh "npm run build" +RUN npm run build FROM gitea.pena/penadevops/container-images/nginx:main as result diff --git a/deployments/main/docker-compose.yaml b/deployments/main/docker-compose.yaml index f4a8d65..56dcf1e 100644 --- a/deployments/main/docker-compose.yaml +++ b/deployments/main/docker-compose.yaml @@ -3,6 +3,7 @@ services: container_name: hub restart: unless-stopped hostname: hub - image: gitea.pena/penaside/front-hub/main:1118 + image: gitea.pena/penaside/front-hub/main:$latest tty: true + pull_policy: always diff --git a/deployments/staging/docker-compose.yaml b/deployments/staging/docker-compose.yaml index 5dbfb65..0f04f3d 100644 --- a/deployments/staging/docker-compose.yaml +++ b/deployments/staging/docker-compose.yaml @@ -2,7 +2,8 @@ services: hub: container_name: hub restart: unless-stopped - image: gitea.pena/penaside/front-hub/staging:$GITHUB_RUN_NUMBER + image: gitea.pena/penaside/front-hub/staging:latest hostname: hub tty: true + pull_policy: always diff --git a/package.json b/package.json index 2dd8f33..f460a96 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,13 @@ "scripts": { "start": "NODE_OPTIONS=\"--max-old-space-size=1024\" craco start", "build": "craco build", + "build:check": "tsc --noEmit && craco build", + "type-check": "tsc --noEmit", + "type-check:strict": "tsc --noEmit --strict", + "pre-commit": "npm run type-check", "test": "craco test --env=node --transformIgnorePatterns \"node_modules/(?!@frontend)/\"", "test:cart": "vitest ./src/utils/calcCart", + "deploy": "docker login gitea.pena && docker build -t gitea.pena/penaside/front-hub/$(git branch --show-current):latest . && docker push gitea.pena/penaside/front-hub/$(git branch --show-current):latest", "eject": "craco eject", "test:cypress": "start-server-and-test start http://localhost:3000 cypress", "cypress": "cypress open", @@ -16,9 +21,10 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", - "@frontend/kitui": "^1.0.108", + "@frontend/kitui": "^1.0.110", "@mui/icons-material": "^5.10.14", "@mui/material": "^5.10.14", + "@mui/x-date-pickers": "^7.13.0", "@popperjs/core": "^2.11.8", "axios": "^1.4.0", "buffer": "^6.0.3", @@ -29,6 +35,7 @@ "immer": "^10.0.2", "isomorphic-fetch": "^3.0.0", "js-big-decimal": "^2.0.7", + "moment": "^2.30.1", "notistack": "^3.0.1", "pdfjs-dist": "3.6.172", "react": "^18.2.0", @@ -39,6 +46,7 @@ "react-slick": "^0.30.3", "slick-carousel": "^1.8.1", "swr": "^2.2.5", + "transliteration": "^2.3.5", "use-debounce": "^10.0.0", "web-vitals": "^2.1.0", "yup": "^1.1.1", diff --git a/src/api/auth.ts b/src/api/auth.ts index aaa65d8..b4de6be 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,4 +1,4 @@ -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@root/utils/parse-error"; @@ -12,7 +12,7 @@ import type { type RecoverResponse = { message: string; }; - +//tsignore const API_URL = `${process.env.REACT_APP_DOMAIN}/auth`; export const register = async ( diff --git a/src/api/cart.ts b/src/api/cart.ts index ed136e4..b06da10 100644 --- a/src/api/cart.ts +++ b/src/api/cart.ts @@ -1,9 +1,10 @@ import { UserAccount } from "@frontend/kitui"; -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; +import Cart from "@root/pages/Cart/Cart"; import { parseAxiosError } from "@root/utils/parse-error"; -const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.0`; +const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1`; export const patchCart = async ( tariffId: string diff --git a/src/api/history.ts b/src/api/history.ts index d44846f..d7e36b6 100644 --- a/src/api/history.ts +++ b/src/api/history.ts @@ -1,6 +1,6 @@ import { Tariff } from "@frontend/kitui"; import { parseAxiosError } from "@root/utils/parse-error"; -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; export interface GetHistoryResponse { totalPages: number; @@ -44,7 +44,7 @@ export type RawDetails = { Value: string | number | KeyValue[][]; }; -const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.0`; +const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1`; export const getHistory = async (): Promise< [GetHistoryResponse | GetHistoryResponse2 | null, string?] diff --git a/src/api/makeRequest.ts b/src/api/makeRequest.ts deleted file mode 100644 index a356f5c..0000000 --- a/src/api/makeRequest.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as KIT from "@frontend/kitui"; -import { Method, ResponseType, AxiosError } from "axios"; -import { clearAuthToken } from "@frontend/kitui"; -import { clearUserData } from "@root/stores/user"; -import { clearCustomTariffs } from "@root/stores/customTariffs"; -import { clearTickets } from "@root/stores/tickets"; -import { redirect } from "react-router-dom"; -import { setNotEnoughMoneyAmount } from "@stores/cart"; - -interface MakeRequest { - method?: Method | undefined; - url: string; - body?: unknown; - useToken?: boolean | undefined; - contentType?: boolean | undefined; - responseType?: ResponseType | undefined; - signal?: AbortSignal | undefined; - withCredentials?: boolean | undefined; -} -interface ErrorResponseData { - message?: string; -} - -async function makeRequest( - data: MakeRequest -): Promise { - try { - const response = await KIT.makeRequest(data); - - return response as TResponse; - } catch (e) { - const error = e as AxiosError; - if ( - error.response?.status === 400 && - (error.response?.data as ErrorResponseData)?.message === - "refreshToken is empty" - ) { - clearAuthToken(); - clearUserData(); - clearCustomTariffs(); - clearTickets(); - setNotEnoughMoneyAmount(0); - redirect("/"); - } - throw e; - } -} -export default makeRequest; diff --git a/src/api/price.ts b/src/api/price.ts index a411f78..3020634 100644 --- a/src/api/price.ts +++ b/src/api/price.ts @@ -1,4 +1,4 @@ -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import type { GetDiscountsResponse } from "@root/model/discount"; import { useUserStore } from "@root/stores/user"; import { parseAxiosError } from "@root/utils/parse-error"; diff --git a/src/api/promocode.ts b/src/api/promocode.ts index eab67f2..14dc7b9 100644 --- a/src/api/promocode.ts +++ b/src/api/promocode.ts @@ -1,4 +1,4 @@ -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@utils/parse-error"; diff --git a/src/api/recentlyPurchasedTariffs.ts b/src/api/recentlyPurchasedTariffs.ts index d154faa..a02acca 100644 --- a/src/api/recentlyPurchasedTariffs.ts +++ b/src/api/recentlyPurchasedTariffs.ts @@ -1,7 +1,7 @@ -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@root/utils/parse-error"; -const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.0`; +const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1`; type GetRecentlyPurchasedTariffsResponse = { id: string; diff --git a/src/api/tariff.ts b/src/api/tariff.ts index bb704cb..896e6cd 100644 --- a/src/api/tariff.ts +++ b/src/api/tariff.ts @@ -1,4 +1,4 @@ -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { Tariff } from "@frontend/kitui"; import { parseAxiosError } from "@root/utils/parse-error"; @@ -8,6 +8,7 @@ import type { } from "@root/model/privilege"; import type { GetTariffsResponse } from "@root/model/tariff"; import { removeTariffFromCart } from "@root/stores/user"; +import axios from "axios"; interface CreateTariffBody { name: string; @@ -134,3 +135,24 @@ export const getTariffArray = async (tariffIds: string[] | undefined) => { return tariffs; }; + +const apiUrl = process.env.REACT_APP_DOMAIN + "/requestquiz"; +export async function sendContactFormRequest(body: { + contact: string; + whoami: string; +}) { +try { + const a = await axios(apiUrl + "/callme", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + data: body, +}); +return [a]; +} catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return [null, `Ошибка при отправке запроса. ${error}`]; +} +} \ No newline at end of file diff --git a/src/api/ticket.ts b/src/api/ticket.ts index 9a59f31..9fe9a21 100644 --- a/src/api/ticket.ts +++ b/src/api/ticket.ts @@ -1,4 +1,4 @@ -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { parseAxiosError } from "@root/utils/parse-error"; import { createTicket as createTicketRequest } from "@frontend/kitui"; import type { CreateTicketResponse } from "@frontend/kitui"; @@ -78,19 +78,23 @@ export const sendFile = async ( export const createTicket = async ( ticketNameField: string, ticketBodyField: string, - isToken?: boolean + isToken: boolean = true ): Promise<[CreateTicketResponse | null, string?]> => { try { - const createTicketResponse = await createTicketRequest({ + const createTicketResponse = await makeRequest< + { Title: string; Message: string; System: boolean }, + CreateTicketResponse + >({ + method: "POST", url: `${API_URL}/create`, body: { Title: ticketNameField, Message: ticketBodyField, System: false }, - useToken: isToken + useToken: isToken, }); return [createTicketResponse]; } catch (nativeError) { const [error] = parseAxiosError(nativeError); - return [null, `Не удалось отправить файл. ${error}`]; + return [null, `Не удалось создать тикет. ${error}`]; } }; diff --git a/src/api/user.ts b/src/api/user.ts index a3a62db..f68c305 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,5 +1,5 @@ import { User } from "@frontend/kitui"; -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { PatchUserRequest } from "@root/model/user"; import { parseAxiosError } from "@root/utils/parse-error"; diff --git a/src/api/verification.ts b/src/api/verification.ts index c3d7cbe..9e92962 100644 --- a/src/api/verification.ts +++ b/src/api/verification.ts @@ -1,4 +1,4 @@ -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { jsonToFormdata } from "@root/utils/jsonToFormdata"; import { parseAxiosError } from "@root/utils/parse-error"; @@ -55,7 +55,7 @@ export const sendDocuments = async ( const sendDocumentsResponse = await makeRequest({ method: "POST", url: API_URL, - body: jsonToFormdata({ ...documents, egrule: documents.inn }), + body: jsonToFormdata({ ...documents}), useToken: true, withCredentials: true, }); @@ -76,7 +76,7 @@ export const updateDocuments = async ( method: "PUT", url: `${API_URL}`, body: jsonToFormdata( - documents.inn ? { ...documents, egrule: documents.inn } : documents + documents.inn ? { ...documents} : documents ), useToken: true, withCredentials: true, diff --git a/src/api/wallet.ts b/src/api/wallet.ts index ed1604d..38800f9 100644 --- a/src/api/wallet.ts +++ b/src/api/wallet.ts @@ -1,4 +1,4 @@ -import makeRequest from "@api/makeRequest"; +import { makeRequest } from "@frontend/kitui"; import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet"; import { parseAxiosError } from "@root/utils/parse-error"; @@ -6,8 +6,12 @@ const isStaging = (() => { const host = window.location.hostname; return host.includes("s") ? "s" : ""; })(); +const isLocalhost = (() => { + const host = window.location.hostname; + return host.includes("localhost"); +})(); -const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.0`; +const API_URL = `${process.env.REACT_APP_DOMAIN}/customer/v1.0.1`; interface PaymentBody { type: string; @@ -16,29 +20,32 @@ interface PaymentBody { export const sendPayment = async ({ userId, - wayback, body, - fromSquiz, - paymentPurpose, + action, + fromDomain, + backWay, + additionalinformation, }: { userId: string; - wayback: string; body: PaymentBody; - fromSquiz: boolean; - paymentPurpose: "paycart" | "replenishwallet"; + action?: string; + fromDomain?: string; + backWay?: string; + additionalinformation?: string; }): Promise<[SendPaymentResponse | null, string?]> => { - console.log(" я sendPayment и вот мой wayback = " + wayback) - -let returnUrl= `https://${isStaging}hub.pena.digital/afterpay?from=${ - fromSquiz ? "quiz" : "hub" -}&purpose=${paymentPurpose}&userid=${userId}` - -if (wayback) returnUrl += `&wayback=${wayback}` - -console.log("returnUrl") -console.log(returnUrl) + // Правильно формируем returnUrl + const domain = fromDomain || `${isStaging ? "s" : ""}hub.pena.digital`; + const path = backWay ? `/${backWay}` : ''; + let returnUrl = `https://${domain}${path}?userid=${userId}&action=${action || 'buy'}`; + + // Добавляем additionalinformation если он есть + if (additionalinformation) { + returnUrl += `&additionalinformation=${encodeURIComponent(additionalinformation)}`; + } + + const reqeustBody = { currency: "RUB", bankCard: { diff --git a/src/assets/Icons/CloseIcon.tsx b/src/assets/Icons/CloseIcon.tsx new file mode 100644 index 0000000..7b1bd62 --- /dev/null +++ b/src/assets/Icons/CloseIcon.tsx @@ -0,0 +1,38 @@ +import { useLocation } from "react-router-dom"; +import { Box, SxProps, Theme } from "@mui/material"; + +interface Props { + sx?: SxProps; +} +export default function CloseIcon({ sx }: Props) { + const location = useLocation(); + return ( + + + + + + ); +} diff --git a/src/assets/Icons/NotebookWithPencil copy.tsx b/src/assets/Icons/NotebookWithPencil copy.tsx new file mode 100644 index 0000000..c4581d1 --- /dev/null +++ b/src/assets/Icons/NotebookWithPencil copy.tsx @@ -0,0 +1,22 @@ +import { useLocation } from "react-router-dom"; +import { Box, SxProps, Theme } from "@mui/material"; + +interface Props { + sx?: SxProps; +} +export default function NotebookWithPencil({ sx }: Props) { + return ( + + + + + + + + ); +} \ No newline at end of file diff --git a/src/assets/Icons/NotebookWithPencil.tsx b/src/assets/Icons/NotebookWithPencil.tsx new file mode 100644 index 0000000..ca0c440 --- /dev/null +++ b/src/assets/Icons/NotebookWithPencil.tsx @@ -0,0 +1,20 @@ +import { useLocation } from "react-router-dom"; +import { Box, SxProps, Theme } from "@mui/material"; + +interface Props { + sx?: SxProps; +} +export default function CloseIcon({ sx }: Props) { + const location = useLocation(); + return ( + + + + + + + + ); +} \ No newline at end of file diff --git a/src/components/CustomTextField.tsx b/src/components/CustomTextField.tsx new file mode 100755 index 0000000..5090f98 --- /dev/null +++ b/src/components/CustomTextField.tsx @@ -0,0 +1,152 @@ +import type { ChangeEvent, FocusEvent, KeyboardEvent } from "react"; +import React, { useEffect, useState } from "react"; +import type { InputProps, SxProps, Theme } from "@mui/material"; +import { + Box, + FormControl, + Input, + InputLabel, + Typography, + useTheme, +} from "@mui/material"; + +interface CustomTextFieldProps { + placeholder: string; + id?: string; + value?: string; + error?: string; + emptyError?: boolean; + onChange?: (event: ChangeEvent) => void; + onKeyDown?: (event: KeyboardEvent) => void; + onBlur?: (event: FocusEvent) => void; + text?: string; + maxLength?: number; + sx?: SxProps; + sxForm?: SxProps; + InputProps?: Partial; + type?: string; + rows?: number; + className?: string; + disabled?: boolean; +} + +export default function CustomTextField({ + placeholder, + id, + value = "", + onChange, + onKeyDown, + onBlur, + text, + sx, + error, + emptyError, + InputProps, + maxLength = 200, + type = "", + rows = 0, + sxForm, + className, + disabled, +}: CustomTextFieldProps) { + const theme = useTheme(); + + const [inputValue, setInputValue] = useState(""); + const [isInputActive, setIsInputActive] = useState(false); + + useEffect(() => { + setInputValue(value); + }, [value]); + + const handleInputChange = (event: React.ChangeEvent) => { + if (event.target.value.length <= maxLength) { + const inputValue = event.target.value; + + if (type === "number") { + setInputValue(inputValue.replace(/\D/g, "")); + } else { + setInputValue(inputValue); + } + + if (onChange) { + onChange(event); + } + } + }; + + const handleInputFocus = () => { + setIsInputActive(true); + }; + + const handleInputBlur = (event: React.FocusEvent) => { + setIsInputActive(false); + + if (onBlur) { + onBlur(event); + } + }; + + return ( + + {error && ( + + {error} + + )} + 0} + rows={rows} + disabled={disabled} + disableUnderline + sx={{ + maxLength: maxLength, + borderRadius: "10px", + fontSize: "18px", + lineHeight: "21px", + p: "13px", + border: `${isInputActive ? "black 2px" : "#9A9AAF 1px"} solid`, + backgroundColor: theme.palette.background.default, + height: "48px", + ...sx, + }} + data-cy="textfield" + /> + {isInputActive && inputValue.length >= maxLength - 7 && ( + + {inputValue.length} + / + {maxLength} + + )} + + ); +} diff --git a/src/components/CustomWrapperDrawer.tsx b/src/components/CustomWrapperDrawer.tsx index 642b204..09f8966 100644 --- a/src/components/CustomWrapperDrawer.tsx +++ b/src/components/CustomWrapperDrawer.tsx @@ -13,7 +13,7 @@ import { ReactComponent as CrossIcon } from "@root/assets/Icons/cross.svg"; import type { MouseEvent } from "react"; import CustomTariffAccordion from "@root/components/CustomTariffAccordion"; -import { setNotEnoughMoneyAmount } from "@root/stores/cart"; +import { setNotEnoughMoneyAmount } from "@root/stores/allTypesOfPurchases"; const name: Record = { templategen: "Шаблонизатор", diff --git a/src/components/Drawers.tsx b/src/components/Drawers.tsx index 0a94792..58ccf2f 100644 --- a/src/components/Drawers.tsx +++ b/src/components/Drawers.tsx @@ -19,8 +19,8 @@ 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"; -import { setNotEnoughMoneyAmount, useCartStore } from "@root/stores/cart"; -import { useDiffMoney } from "@root/stores/diffMoney"; +import { setNotEnoughMoneyAmount, startPayCartProcess, allTypesOfPurchases } from "@root/stores/allTypesOfPurchases"; +import { RSCOpen } from "@root/stores/requestSquizCreate"; function Drawers() { const [openNotificationsModal, setOpenNotificationsModal] = useState(false); @@ -34,23 +34,30 @@ function Drawers() { const cart = useCart(); console.log("боковой cart", cart.priceAfterDiscounts) const userAccount = useUserStore((state) => state.userAccount); + const userId = useUserStore((state) => state.userId) || ""; const tickets = useTicketStore((state) => state.tickets); - const notEnoughMoneyAmount = useCartStore(state => state.notEnoughMoneyAmount); - const { setNewDiff } = useDiffMoney() + const notEnoughMoneyAmount = allTypesOfPurchases(state => state.notEnoughMoneyAmount); + const siteReadyPayCart = allTypesOfPurchases(state => state.siteReadyPayCart) const notificationsCount = tickets.filter( - ({ user, top_message }) => user !== top_message.user_id && top_message.shown.me !== 1 + ({ user, top_message }) => + user !== top_message.user_id && + top_message.shown.me !== 1 && + !top_message.system ).length; async function handlePayClick() { setLoading(true); + const isCC = cart.services.length > 0 && cart.services[0].tariffs.some(t => t.privileges[0].privilegeId === "quizManual") + const [payCartResponse, payCartError] = await payCart(); if (payCartError) { if (payCartError.includes("insufficient funds: ")) { const notEnoughMoneyAmount = parseInt(payCartError.replace(/^.*insufficient\sfunds:\s(?=\d+$)/, "")); setNotEnoughMoneyAmount(notEnoughMoneyAmount); + startPayCartProcess(userId) } setLoading(false); @@ -60,6 +67,7 @@ function Drawers() { } if (payCartResponse) { + if (isCC) RSCOpen() setUserAccount(payCartResponse); } @@ -69,10 +77,8 @@ function Drawers() { function handleReplenishWallet() { setIsDrawerOpen(false); - if (location.pathname.includes("/payment")) { - setNewDiff(notEnoughMoneyAmount) - } - navigate("/payment", { state: { notEnoughMoneyAmount } }); + if (siteReadyPayCart === null) startPayCartProcess(userId) + navigate("/payment"); } return ( @@ -131,7 +137,10 @@ function Drawers() { setOpen={setOpenNotificationsModal} anchorElement={bellRef.current} notifications={tickets - .filter(({ user, top_message }) => user !== top_message.user_id) + .filter(({ user, top_message }) => + user !== top_message.user_id && + !top_message.system + ) .map((ticket) => ({ text: "У вас новое сообщение от техподдержки", date: new Date(ticket.updated_at).toLocaleDateString(), @@ -180,7 +189,6 @@ function Drawers() { { setIsDrawerOpen(false) - setNotEnoughMoneyAmount(0) }} sx={{ background: "rgba(0, 0, 0, 0.55)" }}> - После нажатия кнопки оплатить, вы будете перенаправлены на форму оплаты, для оплаты ВСЕЙ корзины (рекомендуем перед оплатой, убрать все лишнее) + После нажатия кнопки оплатить (пополнить), вы будете перенаправлены на форму оплаты, для оплаты ВСЕЙ корзины (рекомендуем перед оплатой, убрать все лишнее) ({ enabled: sseEnabled && isActiveSSETab && Boolean(sessionData), url: @@ -160,10 +159,6 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) { `/heruvym/v1.0.0/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`, onNewData: (ticketMessages) => { - console.log("Chat") - - console.log("ticketMessages useSSESubscription ") - console.log(ticketMessages) const isTicketClosed = ticketMessages.some( (message) => message.session_id === "close" ); @@ -176,13 +171,10 @@ export default function Chat({ open = false, onclickArrow, sx }: Props) { return; } -console.log("under checking some close message -----------------------------------------") - updateSSEValue(ticketMessages); addOrUpdateUnauthMessages(ticketMessages); }, onDisconnect: useCallback(() => { - console.log("DISCONNECT") setUnauthIsPreventAutoscroll(false); setSseEnabled(false); }, []), @@ -258,41 +250,42 @@ console.log("under checking some close message --------------------------------- useEffect(() => { if (open && messages.length > 1) { + // Ищем последнее непрочитанное сообщение НЕ от текущего пользователя let lastUnreadMessage = null; - - // Ищем последнее непрочитанное чужое сообщение до первого прочитанного или своего - for (let i = messages.length - 1; i >= 0; i--) { + + for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; - const isOwnMessage = (ticket.sessionData?.sessionId || user) === message.user_id; - // Пропускаем системные сообщения - if (message.ticket_id === "111") { + // Пропускаем системные сообщения (приветствие) + if (message.ticket_id === "111" || message.system) { continue; } - // Если встретили своё сообщение - прерываем поиск + // Определяем, является ли сообщение от текущего пользователя + const currentUserId = user || ticket.sessionData?.sessionId; + const isOwnMessage = currentUserId === message.user_id; + + // Если сообщение от нас - пропускаем if (isOwnMessage) { - break; + continue; } - // Если встретили прочитанное сообщение - прерываем поиск + // Если сообщение уже прочитано - пропускаем if (message.shown?.me === 1) { - break; + continue; } - // Если сообщение чужое и не прочитано - запоминаем его - if (!isOwnMessage && message.shown?.me !== 1) { - lastUnreadMessage = message; - break; - } + // Нашли непрочитанное сообщение не от нас - запоминаем его + lastUnreadMessage = message; + break; } - // Если нашли непрочитанное сообщение - отмечаем его как прочитанное + // Отправляем shown только на последнее непрочитанное сообщение не от нас if (lastUnreadMessage) { shownMessage(lastUnreadMessage.id); - } + } } - }, [open, messages]); + }, [open, messages, user, ticket.sessionData?.sessionId]); const loadNewMessages = ( event: WheelEvent | TouchEvent @@ -463,14 +456,6 @@ console.log("under checking some close message --------------------------------- > онлайн-консультант - - время работы 10:00-3:00 по мск - state.user?._id); - const { messages } = useTicketStore( + const { messages, sessionData } = useTicketStore( (state) => state[user ? "authData" : "unauthData"] ); @@ -72,17 +72,44 @@ export default function FloatingSupportChat() { }; const unreadMessagesCount = useMemo(() => { - let count = 0; + if (messages.length === 0) return 0; - // Идём с конца массива к началу - for (let i = messages.length - 1; i >= 0; i--) { + // Получаем последнее сообщение + const lastMessage = messages[messages.length - 1]; + + // Определяем, является ли сообщение от текущего пользователя + const currentUserId = user || sessionData?.sessionId; + const isLastMessageOwn = currentUserId === lastMessage.user_id; + + // Если последнее сообщение моё - не показываем количество + if (isLastMessageOwn) { + return 0; + } + + // Если последнее сообщение не моё, но прочитано - не показываем количество + if (lastMessage.shown?.me === 1) { + return 0; + } + + // Если последнее сообщение не моё и не прочитано - считаем его +1 + let count = 1; + + // Считаем все предыдущие непрочитанные сообщения до своего + for (let i = messages.length - 2; i >= 0; i--) { const message = messages[i]; - // Пропускаем сообщение с id "111" - if (message.id === "111") continue; + // Пропускаем системные сообщения + if (message.id === "111" || message.system) { + continue; + } + + // Если встретили своё сообщение - прекращаем подсчёт + if (currentUserId === message.user_id) { + break; + } - // Если сообщение не прочитано (shown.me !== 1) - if (message.shown.me !== 1) { + // Если сообщение не прочитано - увеличиваем счетчик + if (message.shown?.me !== 1) { count++; } else { // Встретили прочитанное сообщение - прекращаем подсчёт @@ -91,7 +118,7 @@ export default function FloatingSupportChat() { } return count; - }, [messages]); // Зависимость от messages - пересчитывается при их изменении + }, [messages, user, sessionData?.sessionId]); useEffect(() => { const onResize = () => { diff --git a/src/components/InfoButton.tsx b/src/components/InfoButton.tsx new file mode 100644 index 0000000..2a966e0 --- /dev/null +++ b/src/components/InfoButton.tsx @@ -0,0 +1,50 @@ +import { IconButton, SxProps } from "@mui/material"; + +type InfoProps = { + width?: number; + height?: number; + sx?: SxProps; + onClick?: any; + className?: string; + color?: string; +}; + +export default function InfoButton({ + width = 20, + height = 20, + sx, + onClick, + className, + color = "#7e2aea", +}: InfoProps) { + return ( + + + + + + + + ); +} diff --git a/src/components/NavbarLanding/NavbarFull.tsx b/src/components/NavbarLanding/NavbarFull.tsx index ebfd2be..9ab1066 100644 --- a/src/components/NavbarLanding/NavbarFull.tsx +++ b/src/components/NavbarLanding/NavbarFull.tsx @@ -28,7 +28,8 @@ import { import { currencyFormatter } from "@root/utils/currencyFormatter"; import { clearCustomTariffs } from "@root/stores/customTariffs"; import { clearTickets } from "@root/stores/tickets"; -import {setNotEnoughMoneyAmount} from "@stores/cart" +import {setNotEnoughMoneyAmount} from "@stores/allTypesOfPurchases" +import { logoutAndRedirect } from "@root/utils/logout"; interface Props { isLoggedIn: boolean; @@ -44,15 +45,8 @@ export default function NavbarFull({ isLoggedIn }: Props) { const [open, setOpen] = useState(false); async function handleLogoutClick() { - clearAuthToken(); - clearUserData(); - clearCustomTariffs(); - clearTickets(); - setNotEnoughMoneyAmount(0) - navigate("/"); - + logoutAndRedirect(navigate); const [_, logoutError] = await logout(); - if (logoutError) { return enqueueSnackbar(logoutError); } diff --git a/src/components/NavbarSite/DialogMenu.tsx b/src/components/NavbarSite/DialogMenu.tsx index bee1989..4cc448b 100644 --- a/src/components/NavbarSite/DialogMenu.tsx +++ b/src/components/NavbarSite/DialogMenu.tsx @@ -17,7 +17,8 @@ import { logout } from "@root/api/auth"; import { enqueueSnackbar } from "notistack"; import { clearCustomTariffs } from "@root/stores/customTariffs"; import { clearTickets } from "@root/stores/tickets"; -import {setNotEnoughMoneyAmount} from "@stores/cart" +import {setNotEnoughMoneyAmount} from "@stores/allTypesOfPurchases" +import { logoutAndRedirect } from "@root/utils/logout"; type MenuItem = { name: string; @@ -66,15 +67,8 @@ export default function DialogMenu({ handleClose }: DialogMenuProps) { }; async function handleLogoutClick() { - clearAuthToken(); - clearUserData(); - clearCustomTariffs(); - clearTickets(); - setNotEnoughMoneyAmount(0) - navigate("/"); - + logoutAndRedirect(navigate); const [_, logoutError] = await logout(); - if (logoutError) { return enqueueSnackbar(logoutError); } diff --git a/src/components/NavbarSite/NavbarCollapsed.tsx b/src/components/NavbarSite/NavbarCollapsed.tsx index 8461096..5463e12 100644 --- a/src/components/NavbarSite/NavbarCollapsed.tsx +++ b/src/components/NavbarSite/NavbarCollapsed.tsx @@ -48,7 +48,9 @@ export default function NavbarCollapsed({ children }: Props) { const notificationsCount = tickets.filter( ({ user, top_message }) => - user !== top_message.user_id && top_message.shown.me !== 1 + user !== top_message.user_id && + top_message.shown.me !== 1 && + !top_message.system ).length useEffect(() => { @@ -186,7 +188,10 @@ export default function NavbarCollapsed({ children }: Props) { setOpen={setOpenNotificationsModal} anchorElement={bellRef.current} notifications={tickets - .filter(({ user, top_message }) => user !== top_message.user_id) + .filter(({ user, top_message }) => + user !== top_message.user_id && + !top_message.system + ) .map((ticket) => ({ text: "У вас новое сообщение от техподдержки", date: new Date(ticket.updated_at).toLocaleDateString(), diff --git a/src/components/NavbarSite/NavbarFull.tsx b/src/components/NavbarSite/NavbarFull.tsx index 67bb46e..1816aa7 100644 --- a/src/components/NavbarSite/NavbarFull.tsx +++ b/src/components/NavbarSite/NavbarFull.tsx @@ -17,7 +17,8 @@ import { currencyFormatter } from "@root/utils/currencyFormatter"; import { clearTickets } from "@root/stores/tickets"; import type { ReactNode } from "react"; -import {setNotEnoughMoneyAmount} from "@stores/cart" +import {setNotEnoughMoneyAmount} from "@stores/allTypesOfPurchases" +import { logoutAndRedirect } from "@root/utils/logout"; interface Props { children: ReactNode; @@ -30,15 +31,8 @@ export default function NavbarFull({ children }: Props) { const initials = useUserStore((state) => state.initials); async function handleLogoutClick() { - clearAuthToken(); - clearUserData(); - clearCustomTariffs(); - clearTickets(); - setNotEnoughMoneyAmount(0) - navigate("/"); - + logoutAndRedirect(navigate); const [_, logoutError] = await logout(); - if (logoutError) { return enqueueSnackbar(logoutError); } diff --git a/src/components/NavbarSite/NavbarPanel.tsx b/src/components/NavbarSite/NavbarPanel.tsx index 2627163..3a57c3a 100644 --- a/src/components/NavbarSite/NavbarPanel.tsx +++ b/src/components/NavbarSite/NavbarPanel.tsx @@ -18,7 +18,8 @@ import { clearTickets } from "@root/stores/tickets"; import { currencyFormatter } from "@root/utils/currencyFormatter"; import walletIcon from "@root/assets/Icons/wallet_icon.svg"; -import {setNotEnoughMoneyAmount} from "@stores/cart" +import {setNotEnoughMoneyAmount} from "@stores/allTypesOfPurchases" +import { logoutAndRedirect } from "@root/utils/logout"; export const NavbarPanel = () => { const navigate = useNavigate(); @@ -28,15 +29,8 @@ export const NavbarPanel = () => { const initials = useUserStore((state) => state.initials); async function handleLogoutClick() { - clearAuthToken(); - clearUserData(); - clearCustomTariffs(); - clearTickets(); - setNotEnoughMoneyAmount(0) - navigate("/"); - + logoutAndRedirect(navigate); const [_, logoutError] = await logout(); - if (logoutError) { return enqueueSnackbar(logoutError); } diff --git a/src/components/ProtectedLayout.tsx b/src/components/ProtectedLayout.tsx index 91d77d1..444b1d4 100644 --- a/src/components/ProtectedLayout.tsx +++ b/src/components/ProtectedLayout.tsx @@ -40,7 +40,6 @@ export default function ProtectedLayout() { process.env.REACT_APP_DOMAIN + `/heruvym/v1.0.0/subscribe?Authorization=${token}`, onNewData: (data) => { - console.log("ProtectedLayout") updateSSEValue(data); updateTickets(data.filter((d) => Boolean(d.id))); setTicketCount(data.length); diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index f24a1bf..ee461a1 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -72,10 +72,10 @@ export const Select = ({ items, selectedItem, setSelectedItem }: SelectProps) => className="select" value="" open={opened} + onClick={() => setOpened((isOpened) => !isOpened)} MenuProps={{ disablePortal: true }} sx={{ width: "100%",zIndex: 1, }} onChange={selectItem} - onClick={() => setOpened((isOpened) => !isOpened)} > {items.map((item, index) => ( diff --git a/src/components/TotalPrice.tsx b/src/components/TotalPrice.tsx index 3d2e3f9..89e9a75 100644 --- a/src/components/TotalPrice.tsx +++ b/src/components/TotalPrice.tsx @@ -7,8 +7,10 @@ import { Loader } from "./Loader"; import { currencyFormatter } from "@root/utils/currencyFormatter"; import { payCart } from "@root/api/cart"; -import { setUserAccount } from "@root/stores/user"; -import { setNotEnoughMoneyAmount, useCartStore } from "@root/stores/cart"; +import { setUserAccount, useUserStore } from "@root/stores/user"; +import { setNotEnoughMoneyAmount, startPayCartProcess, allTypesOfPurchases } from "@root/stores/allTypesOfPurchases"; +import { RSCOpen } from "@root/stores/requestSquizCreate"; +import { useCart } from "@root/utils/hooks/useCart"; interface Props { priceBeforeDiscounts: number; @@ -19,13 +21,16 @@ interface Props { export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts, isConstructor = false }: Props) { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); - const notEnoughMoneyAmount = useCartStore(state => state.notEnoughMoneyAmount); + const notEnoughMoneyAmount = allTypesOfPurchases(state => state.notEnoughMoneyAmount); + const userId = useUserStore(store => store.userId) || "" const [loading, setLoading] = useState(false); const navigate = useNavigate(); + const cart = useCart(); async function handlePayClick() { setLoading(true); + const isCC = cart.services.length > 0 && cart.services[0].tariffs.some(t => t.privileges[0].privilegeId === "quizManual") const [payCartResponse, payCartError] = await payCart(); if (payCartError) { @@ -41,6 +46,7 @@ export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts, } if (payCartResponse) { + if (isCC) RSCOpen() setUserAccount(payCartResponse); } @@ -48,6 +54,7 @@ export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts, } function handleReplenishWallet() { + startPayCartProcess(userId) navigate("/payment", { state: { notEnoughMoneyAmount } }); } diff --git a/src/index.tsx b/src/index.tsx index b0c6e67..8cc12ae 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,3 @@ -import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter, @@ -9,10 +8,10 @@ import { useNavigate, } from "react-router-dom"; import { CssBaseline, ThemeProvider } from "@mui/material"; +import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import Faq from "./pages/Faq/Faq"; import Wallet from "./pages/Wallet"; import Payment from "./pages/Payment/Payment"; -import QuizPayment from "./pages/QuizPayment/QuizPayment"; import Support from "./pages/Support/Support"; import ChatImageNewWindow from "./pages/Support/ChatImageNewWindow"; import AccountSettings from "./pages/AccountSettings/AccountSettings"; @@ -35,6 +34,8 @@ import { setUser, setUserAccount, useUserStore, + OriginalUserSquizAccount, + setQuizUserAccount } from "./stores/user"; import TariffConstructor from "./pages/TariffConstructor/TariffConstructor"; import { @@ -44,6 +45,7 @@ import { useUserFetcher, } from "@frontend/kitui"; import { pdfjs } from "react-pdf"; +import { ruRU } from "@mui/x-date-pickers/locales"; import { theme } from "./utils/theme"; import PPofData from "@root/docs/PPofData"; import Docs from "@root/docs/docs"; @@ -53,17 +55,29 @@ import PrivacyPolicy from "@root/docs/content/PrivacyPolicy"; import RecoverPassword from "@root/pages/auth/RecoverPassword"; import OutdatedLink from "@root/pages/auth/OutdatedLink"; import { verify } from "./pages/AccountSettings/helper"; -import AfterPay from "./pages/AfterPay"; import { PageNotFound } from "./pages/PageNotFound"; -import {setNotEnoughMoneyAmount} from "@stores/cart" +import { usePipeSubscriber } from "./utils/hooks/usePipeSubscriber"; +import { LocalizationProvider } from "@mui/x-date-pickers"; +import { ModalRequestCreate } from "./pages/Tariffs/ModalRequestCreate"; +import * as crutch from "./useUserAccountFetcher"; +import { useTryBuy } from "./utils/hooks/useTryBuy"; +import AnyServicePayment from "./pages/AnyServicePayment/AnyServicePayment"; +import { ErrorBoundary } from "react-error-boundary"; +import { ApologyFallback } from "./pages/ApologyPage"; +import { handleComponentError } from "./utils/handleComponentError"; +import { createMakeRequestConfig } from "@frontend/kitui"; + +console.log("Я здесь для отладки и спешу сообщить, что деплой был успешно завершен!") pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`; +const localeText = + ruRU.components.MuiLocalizationProvider.defaultProps.localeText; const App = () => { const location = useLocation(); - const userId = useUserStore((state) => state.userId); + const userId = useUserStore(state => state.userId); const navigate = useNavigate(); - if(location.pathname !== "/cart"){setNotEnoughMoneyAmount(0)} + useUserFetcher({ url: process.env.REACT_APP_DOMAIN + `/user/${userId}`, userId, @@ -79,7 +93,7 @@ const App = () => { }); useUserAccountFetcher({ - url: process.env.REACT_APP_DOMAIN + "/customer/v1.0.0/account", + url: process.env.REACT_APP_DOMAIN + "/customer/v1.0.1/account", userId, onNewUserAccount: setUserAccount, onError: (error) => { @@ -93,6 +107,24 @@ const App = () => { }, }); + crutch.useUserAccountFetcher({ + url: `${process.env.REACT_APP_DOMAIN}/squiz/account/get`, + userId, + onNewUserAccount: setQuizUserAccount, + onError: (error) => { + const errorMessage = getMessageFromFetchError(error); + if (errorMessage) { + enqueueSnackbar(errorMessage); + clearUserData(); + clearAuthToken(); + navigate("/signin"); + } + }, + }); + + usePipeSubscriber(); + useTryBuy(); + verify(userId); if (location.state?.redirectTo) @@ -115,6 +147,9 @@ const App = () => { } /> )} + + + } /> { to="/" replace state={{ - redirectTo: window.location.pathname + window.location.search, + redirectTo: `/changepwd${location.search}`, }} /> } @@ -159,6 +194,7 @@ const App = () => { /> } /> + }> }> } /> @@ -179,8 +215,8 @@ const App = () => { /> + } /> } /> - } /> }> } /> { /> } /> - } /> } /> ); }; +createMakeRequestConfig(undefined, handleComponentError, () => []); + const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); root.render( // - - - - - - - + + + + + + + + + + + // ); diff --git a/src/model/auth.ts b/src/model/auth.ts index 0050435..fdcbdd6 100644 --- a/src/model/auth.ts +++ b/src/model/auth.ts @@ -1,4 +1,5 @@ import type { Attachment } from "@root/model/attachment" +import { UserAccount } from "@frontend/kitui" export type File = { name: "inn" | "rule" | "egrule" | "certificate"; @@ -8,21 +9,21 @@ export type File = { export interface Verification { _id: string; accepted: boolean; - status: "org" | "nko"; + status: UserAccount["status"]; updated_at: string; comment: string; files: File[]; } export type SendDocumentsArgs = { - status: "org" | "nko"; + status: UserAccount["status"]; inn: Attachment; rule: Attachment; certificate?: Attachment; }; export type UpdateDocumentsArgs = { - status: "org" | "nko"; + status: UserAccount["status"]; inn?: Attachment; rule?: Attachment; certificate?: Attachment; diff --git a/src/model/autoPay.ts b/src/model/autoPay.ts new file mode 100644 index 0000000..7a77bf0 --- /dev/null +++ b/src/model/autoPay.ts @@ -0,0 +1,2 @@ +export type FromDomain = "quiz" | "squiz" | "hub" | "shub" | "localhost:3000"; +export type Action = "topupwallet" | "createquizcc" | "buy"; \ No newline at end of file diff --git a/src/pages/AccountSettings/AccountSettings.tsx b/src/pages/AccountSettings/AccountSettings.tsx index 10a3649..567ee66 100644 --- a/src/pages/AccountSettings/AccountSettings.tsx +++ b/src/pages/AccountSettings/AccountSettings.tsx @@ -33,6 +33,8 @@ function AccountSettings() { const comment = useUserStore((state) => state.comment) const userId = useUserStore((state) => state.userId) ?? "" + console.log(user) + const [onChangeTypeLP, setOnChangeTypeLP] = useState("") const [readySend, setReadySend] = useState(false) @@ -51,10 +53,10 @@ function AccountSettings() { const type = onChangeTypeLP setOnChangeTypeLP("") setReadySend(false) - if (type === "email" && user?.email !== settingsFields.email.value) setSettingsField("email", user?.email || "") + if (type === "email" && user?.email !== settingsFields.email.value) setSettingsField("email", user?.email || user?.login || "") if (type === "password") setSettingsField("password", "") if (type === "all") { - setSettingsField("email", user?.email || "") + setSettingsField("email", user?.email || user?.login || "") setSettingsField("password", "") } } diff --git a/src/pages/AccountSettings/DocumentsDialog/DocumentItem.tsx b/src/pages/AccountSettings/DocumentsDialog/DocumentItem.tsx index f03b57d..c8f8699 100644 --- a/src/pages/AccountSettings/DocumentsDialog/DocumentItem.tsx +++ b/src/pages/AccountSettings/DocumentsDialog/DocumentItem.tsx @@ -67,7 +67,7 @@ export default function DocumentItem({ .then(e => { setReadyShowDocument(true) }) - .catch(e => console.log(e)) + .catch(e => console.error(e)) } } }, []) diff --git a/src/pages/AccountSettings/DocumentsDialog/DocumentUploadItem.tsx b/src/pages/AccountSettings/DocumentsDialog/DocumentUploadItem.tsx index 3687f90..f2a5ca1 100644 --- a/src/pages/AccountSettings/DocumentsDialog/DocumentUploadItem.tsx +++ b/src/pages/AccountSettings/DocumentsDialog/DocumentUploadItem.tsx @@ -47,7 +47,7 @@ export default function DocumentUploadItem({ .then(e => { setReadyShowDocument(true) }) - .catch(e => console.log(e)) + .catch(e => console.error(e)) } } else { setReadyShowDocument(true) diff --git a/src/pages/AccountSettings/UserFields.tsx b/src/pages/AccountSettings/UserFields.tsx index 946d69d..96287e3 100644 --- a/src/pages/AccountSettings/UserFields.tsx +++ b/src/pages/AccountSettings/UserFields.tsx @@ -19,7 +19,7 @@ export default function UserFields({ const upMd = useMediaQuery(theme.breakpoints.up("md")) const { settingsFields, user } = useUserStore((state) => state) - + const a = useUserStore((state) => state) const textFieldProps = { gap: upMd ? "16px" : "10px", diff --git a/src/pages/AfterPay/index.tsx b/src/pages/AfterPay/index.tsx deleted file mode 100644 index 6fc5036..0000000 --- a/src/pages/AfterPay/index.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useState, useEffect } from "react"; -import { - Box, - Button, - Typography, - useTheme, - useMediaQuery, -} from "@mui/material"; -import { payCart } from "@root/api/cart"; -import { useUserStore } from "@root/stores/user"; -import wallet_icon from "@root/assets/Icons/ColorWallet.svg"; -import { Link, useSearchParams } from "react-router-dom"; - -const MINUTE = 1000 * 60; - -export default () => { - const [redirectUrl, setRedirectUrl] = useState("/"); - const theme = useTheme(); - const phone = useMediaQuery(theme.breakpoints.down(375)); - const userId = useUserStore((state) => state.user?._id); - const [searchParams] = useSearchParams(); - const paymentUserId = searchParams.get("userid"); - const wayback = searchParams.get("wayback"); - - useEffect(() => { - const from = searchParams.get("from") || "hub"; - const purpose = searchParams.get("purpose"); - - if (purpose === "paycart" || from !== "quiz") { - let tryCount = 0; - - if (userId !== paymentUserId) { - return; - } - - const payCartPendingRequestDeadline = localStorage.getItem( - "payCartPendingRequestDeadline" - ); - const deadline = payCartPendingRequestDeadline - ? Number(payCartPendingRequestDeadline) - : Date.now() + 20 * MINUTE; - - localStorage.setItem( - "payCartPendingRequestDeadline", - deadline.toString() - ); - - tryPayCart(); - - async function tryPayCart() { - tryCount += 1; - - - if (Date.now() > deadline) { - localStorage.removeItem("payCartPendingRequestDeadline"); - return; - } - - const [, payCartError] = await payCart(); - - if (!payCartError) { - localStorage.removeItem("payCartPendingRequestDeadline"); - return; - } - - setTimeout(tryPayCart, tryCount > 5 ? MINUTE : MINUTE / 6); - } - } - - const host = window.location.hostname; - const domain = (host.includes("s") ? "s" : "") + from; - const pathname = from === "hub" ? "/tariffs" : "/list"; - - let redirect = `https://${domain}.pena.digital${pathname}?afterpay=${true}&userid=${userId}` - if (wayback) redirect += `&wayback=${wayback}` - - setRedirectUrl(redirect); - }, []); - - return ( - - - - - Спасибо за оплату! - - - Ваш платеж принят и в течение 10 минут товары будут зачислены - - - - - ); -}; - -//https://shub.pena.digital/quizpayment?action=squizpay&dif=50000&data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY4NDZlODc4hLWF1d__aACP1IpA&userid=6846e878c149e4d24ebf50f3&from=AI&wayback=ai_27964 \ No newline at end of file diff --git a/src/pages/AnyServicePayment/AnyServicePayment.tsx b/src/pages/AnyServicePayment/AnyServicePayment.tsx new file mode 100644 index 0000000..a709bac --- /dev/null +++ b/src/pages/AnyServicePayment/AnyServicePayment.tsx @@ -0,0 +1,102 @@ +import { useEffect, useState } from "react"; +import { ApologyPage } from "../ApologyPage"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { + clearAuthToken, + getMessageFromFetchError, + setAuthToken, + useUserAccountFetcher, + useUserFetcher, + getAuthToken, +} from "@frontend/kitui"; +import { + clearUserData, + setUserId, + useUserStore, +} from "@root/stores/user"; +import { logout } from "@root/api/auth"; +import { clearCustomTariffs } from "@root/stores/customTariffs"; +import { clearTickets } from "@root/stores/tickets"; +import { cancelPayCartProcess, setAction, setBackWay, setFromDomain, setNotEnoughMoneyAmount, startPayCartProcess } from "@stores/allTypesOfPurchases" +import { Action, FromDomain } from "@root/model/autoPay"; +import { logoutAndRedirect, clearUserSession } from "@root/utils/logout"; +//http://localhost:3001/payment?fromdomain=localhost:3000&action=buy&dif=96900&userid=68773ec66816e2c659b36dad&sec= +let first = true; + +export default function AnyServicePayment() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const [message, setMessage] = useState("Идёт загрузка"); + const user = useUserStore((state) => state.user); + + let URLfromDomain = searchParams.get("fromdomain");//домен откуда произошёл запрос на финансовые работы + let URLaction = searchParams.get("action");//что мы, собсна, хотим: оплатить, пополнить, купить и создать квиз + let URLuserId = searchParams.get("userid");//тот кто начал всё это действо + const URLwayBack = searchParams.get("wayback") ?? "";//путь по которому нужно будет пройти после того как закончим манипуляции (без домена) + let URLmoneyDifferent = Number(searchParams.get("dif"));//сколько нужно деняк + let URLtoken = searchParams.get("sec"); + const URLadditionalinformation = searchParams.get("additionalinformation") ?? "";//дополнительная информация + + useEffect( + function redirectIfSignedIn() { + console.log("---------------------------") + console.log(URLfromDomain) + console.log(URLaction) + console.log(URLuserId) + console.log(URLwayBack) + console.log(URLmoneyDifferent) + console.log(URLtoken) + console.log(URLadditionalinformation) + console.log("---------------------------") + if (!first && user?._id === URLuserId) { + let returnUrl = `/payment?fromdomain=${URLfromDomain}&action=${URLaction}&dif=${URLmoneyDifferent}&userid=${URLuserId}` + if (URLwayBack) returnUrl += `&wayback=${URLwayBack}` + if (URLadditionalinformation) returnUrl += `&additionalinformation=${URLadditionalinformation}` + console.log("AnyServicePayment - формируем URL для payment:", returnUrl); + navigate(returnUrl, { + replace: true, + }); + } + }, + [navigate, user] + ); + + if (first) { + navigate(`/anyservicepayment`, { + replace: true, + }); + try { + first = false; + + if (user?._id === URLuserId) { + return; + } + + if (URLaction && URLmoneyDifferent && URLtoken) { + (async () => { + if (getAuthToken()) { + logoutAndRedirect(navigate); + await logout(); + } + + setAuthToken(URLtoken); + setUserId(URLuserId); + return; + })(); + } else { + var link = document.createElement("a"); + link.href = `https://${URLfromDomain}/${URLwayBack}`; + document.body.appendChild(link); + link.click(); + } + } catch (e) { + setMessage("Произошла ошибка"); + var link = document.createElement("a"); + link.href = `https://${URLfromDomain}/${URLwayBack}`; + document.body.appendChild(link); + link.click(); + } + } + + return ; +} diff --git a/src/pages/ApologyPage.tsx b/src/pages/ApologyPage.tsx index e6c9474..7be04af 100644 --- a/src/pages/ApologyPage.tsx +++ b/src/pages/ApologyPage.tsx @@ -1,4 +1,5 @@ import { Box, Typography } from "@mui/material"; +import { FallbackProps } from "react-error-boundary"; export const ApologyPage = ({ message }: { message: string }) => { return ( @@ -20,3 +21,7 @@ export const ApologyPage = ({ message }: { message: string }) => { ); }; + +export function ApologyFallback({ error }: FallbackProps) { + return ; +} diff --git a/src/pages/Landing/Section2.tsx b/src/pages/Landing/Section2.tsx index b2c4e4b..a72a95d 100644 --- a/src/pages/Landing/Section2.tsx +++ b/src/pages/Landing/Section2.tsx @@ -63,7 +63,7 @@ export default function Section2() { {/* {upMd ? : }*/} - {upMd ? + {upMd ? : (""); - const [selectedPaymentMethod, setSelectedPaymentMethod] = - useState(""); - const [warnModalOpen, setWarnModalOpen] = useState(false); - const [sorryModalOpen, setSorryModalOpen] = useState(false); - const [paymentValueField, setPaymentValueField] = useState("0"); - const [paymentLink, setPaymentLink] = useState(""); - const location = useLocation(); - const verificationStatus = useUserStore((state) => state.verificationStatus); - const userId = useUserStore((state) => state.userId); + const navigate = useNavigate(); const handleCustomBackNavigation = useHistoryTracker(); + const userId = useUserStore((state) => state.userId) || ""; + const notEnoughMoneyAmount = allTypesOfPurchases(state => state.notEnoughMoneyAmount); + const siteReadyPayCart = allTypesOfPurchases(state => state.siteReadyPayCart) + const verificationStatus = useUserStore((state) => state.verificationStatus); + const action = allTypesOfPurchases(state => state.action); + const fromDomain = allTypesOfPurchases(state => state.fromDomain); + const backWay = allTypesOfPurchases(state => state.backWay); + const additionalinformation = allTypesOfPurchases(state => state.additionalinformation); - const [fromSquiz, setIsFromSquiz] = useState(false); - const [waybackToSomeSite, setWaybackToSomeSite] = useState(""); + console.log("Payment - значения из Zustand store:"); + console.log("action:", action); + console.log("fromDomain:", fromDomain); + console.log("backWay:", backWay); + console.log("additionalinformation:", additionalinformation); + console.log("notEnoughMoneyAmount:", notEnoughMoneyAmount); + const [promocodeField, setPromocodeField] = useState(""); + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(""); - const { diffMoney, setNewDiff } = useDiffMoney() + const [paymentValueField, setPaymentValueField] = useAutoPay(); - const notEnoughMoneyAmount = - (location.state?.notEnoughMoneyAmount as number) ?? 0; + const [warnModalOpen, setWarnModalOpen] = useState(false); + const [sorryModalOpen, setSorryModalOpen] = useState(false); - const paymentValue = parseFloat( - bigDecimal.multiply(parseFloat(paymentValueField), 100) + // paymentValue в копейках для API + const paymentValue = Number( + bigDecimal.multiply(parseFloat(paymentValueField || "0"), 100) ); useEffect(() => { - if (diffMoney > 0) { - setNewDiff(0); - setPaymentValueField((diffMoney / 100).toString()); + return () => { + cancelPayCartProcess(); } - }, [diffMoney]) + }, []) - useLayoutEffect(() => { - setPaymentValueField((notEnoughMoneyAmount / 100).toString()); - const params = new URLSearchParams(window.location.search); - const fromSquiz = params.get("action"); - if (fromSquiz === "squizpay") { - setIsFromSquiz(true); - setPaymentValueField((Number(params.get("dif") || "0") / 100).toString()); - } - - const wayback = params.get("wayback"); - if (wayback) setWaybackToSomeSite(wayback); - console.log(" я получил wayback = " + wayback) - - navigate(`/payment`, { - replace: true, - }); - }, []); - - async function handleChoosePaymentClick() { - if (!selectedPaymentMethod) { - enqueueSnackbar("Введите метод оплаты"); - return; - } - - if (Number(paymentValueField) === 0) { + //https://shub.pena.digital/quizpayment?action=squizpay&dif=9800&data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2ODVhNTc4OTgzZWU3N2Y4ZTFlNjNkYyIsImF1ZCI6InBlbmEiLCJpc3MiOiJwZW5hLWF1dGgtc2VydmljZSIsImlhdCI6MTcyMjIyMjgzMywiZXhwIjoxNzI3NDA2ODMzfQ.My1KJWFk034MiMdImQSlzf5p4Sn5Dhboj2VvPQteh59tD_CwXyPtePEyev3thV_58IbOOgJ5cgeBm0JKn7atgMgRMpNQVdeYKtf6HYvVoAqkrMcT1LHgAlEQ0TcaXssFKCQGuiCVltHY3UE-kQv5TeydBpO3U9BDKvMqRqv5-Xo&userid=6685a578983ee77f8e1e63dc + const handlePaymentClick = () => { + // Проверяем оба источника данных + const hasPaymentValue = Number(paymentValueField) > 0; + const hasNotEnoughMoney = notEnoughMoneyAmount > 0; + + console.log("handlePaymentClick - проверка:"); + console.log("paymentValueField:", paymentValueField); + console.log("Number(paymentValueField):", Number(paymentValueField)); + console.log("notEnoughMoneyAmount:", notEnoughMoneyAmount); + console.log("hasPaymentValue:", hasPaymentValue); + console.log("hasNotEnoughMoney:", hasNotEnoughMoney); + + if (!hasPaymentValue && !hasNotEnoughMoney) { enqueueSnackbar("Введите сумму"); return; } - - if (selectedPaymentMethod !== "rspay") { - const [sendPaymentResponse, sendPaymentError] = await sendPayment({ - userId: userId ?? "", - fromSquiz, - wayback: waybackToSomeSite, - body: { - type: selectedPaymentMethod, - amount: Number( - bigDecimal.floor( - bigDecimal.multiply(Number(paymentValueField), 100) - ) - ), - }, - paymentPurpose: notEnoughMoneyAmount ? "paycart" : "replenishwallet", - }); - - if (sendPaymentError) { - return enqueueSnackbar(sendPaymentError); + + if (selectedPaymentMethod === "rspay") { + const amountToCheck = hasPaymentValue ? Number(paymentValueField) : Number(bigDecimal.divide(notEnoughMoneyAmount, 100)); + if (amountToCheck < 900) { + enqueueSnackbar("Минимальная сумма 900р"); + return; } - - if (sendPaymentResponse) { - setPaymentLink(sendPaymentResponse.link); - } - - return; - } else { if (verificationStatus !== VerificationStatus.VERIFICATED) { setWarnModalOpen(true); - return; } - - if (Number(paymentValueField) < 900) { - enqueueSnackbar("Минимальная сумма 900р"); - - return; - } - - const [sendRSPaymentResponse] = await sendRSPayment( - Number(paymentValueField) - ); - - if (sendRSPaymentResponse) { - return enqueueSnackbar(sendRSPaymentResponse); - } - - enqueueSnackbar( - "Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг." - ); - - navigate("/settings"); + startPayRS() + } else { + startPayCard() } } + const startPayRS = async () => { + // Определяем сумму для оплаты + const hasPaymentValue = Number(paymentValueField) > 0; + const amountInRubles = hasPaymentValue ? Number(paymentValueField) : Number(bigDecimal.divide(notEnoughMoneyAmount, 100)); + + const [sendRSPaymentResponse] = await sendRSPayment(amountInRubles); + + if (sendRSPaymentResponse) { + return enqueueSnackbar(sendRSPaymentResponse); + } + + enqueueSnackbar( + "Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг." + ); + + navigate("/settings"); + } + + const startPayCard = async () => { + + if (!selectedPaymentMethod) { + enqueueSnackbar("Введите метод оплаты"); + return + } + + // Определяем сумму для оплаты + const hasPaymentValue = Number(paymentValueField) > 0; + const amountInRubles = hasPaymentValue ? Number(paymentValueField) : Number(bigDecimal.divide(notEnoughMoneyAmount, 100)); + const amountInKopecks = Number(bigDecimal.floor(bigDecimal.multiply(amountInRubles, 100))); + + console.log("startPayCard - параметры:"); + console.log("action:", action); + console.log("fromDomain:", fromDomain); + console.log("backWay:", backWay); + console.log("amountInRubles:", amountInRubles); + console.log("amountInKopecks:", amountInKopecks); + + const [sendPaymentResponse, sendPaymentError] = await sendPayment({ + userId, + body: { + type: selectedPaymentMethod, + amount: amountInKopecks, + }, + action: action || undefined, + fromDomain: fromDomain || undefined, + backWay: backWay || undefined, + additionalinformation: additionalinformation || undefined, + }); + + if (sendPaymentError) { + return enqueueSnackbar(sendPaymentError); + } + + //Произошёл запрос на пополнение счёта. Нам вернули ссылку для перехода на страницу пополнения. + if (sendPaymentResponse) { + document.location.href = sendPaymentResponse.link; + } + } + + async function handleApplyPromocode() { if (!promocodeField) return; @@ -257,7 +270,6 @@ export default function Payment() { image={image} onClick={() => { setSelectedPaymentMethod(name); - setPaymentLink(""); }} unpopular={false} /> @@ -268,7 +280,6 @@ export default function Payment() { image={rsPayLogo} onClick={async () => { setSelectedPaymentMethod("rspay"); - setPaymentLink(""); }} unpopular={false} /> @@ -304,76 +315,59 @@ export default function Payment() { > {upMd && Выберите способ оплаты} К оплате - {paymentLink ? ( - - {currencyFormatter.format( - Number(bigDecimal.divide(bigDecimal.floor(paymentValue), 100)) - )} - - ) : ( - { - const value = parseFloat( - e.target.value.replace(/^0+(?=\d\.)/, "") - ); - setPaymentValueField(isNaN(value) ? "" : value.toString()); - }} - id="payment-amount" - gap={upMd ? "16px" : "10px"} - color={"#F2F3F7"} - FormInputSx={{ mb: "28px" }} - /> - )} + { + siteReadyPayCart?.[userId] && notEnoughMoneyAmount > 0 ? + + {currencyFormatter.format( + Number(bigDecimal.divide(notEnoughMoneyAmount, 100)) + )} + + : + { + const value = parseFloat( + e.target.value.replace(/^0+(?=\d\.)/, "") + ); + setPaymentValueField(isNaN(value) ? "" : value.toString()); + }} + id="payment-amount" + gap={upMd ? "16px" : "10px"} + color={"#F2F3F7"} + FormInputSx={{ mb: "28px" }} + /> + } + - {paymentLink ? ( - - ) : ( - - )} + @@ -381,4 +375,4 @@ export default function Payment() { ); } -//https://shub.pena.digital/quizpayment?action=squizpay&dif=50000&data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY4NDcxNjAxYzE0OWU0ZDI0ZWJmNTExNyIsImF1ZCI6InBlbmEiLCJpc3MiOiJwZW5hLWF1dGgtc2VydmljZSIsImlhdCI6MTc0OTQ4OTE1MywiZXhwIjoxNzU0NjczMTUzfQ.SqEXFSPzP3ugIscqLywGkjFJmFqx13zOtxGAjZQ6SzUw8w9ZXjE9uFn8VbLBMGPqeJbvcT2jRV2bB5qtqMy1T3aNuSZ9AZW0jY1hGvFB-bSrYguMV1yErLkR45SvdK2jGI6dg3p6LRqCHb2JMl8vur_KKaus3GiJlsTNNR0fhgI&userid=68471601c149e4d24ebf5117&from=AI&wayback=ai_27969 \ No newline at end of file +//https://shub.pena.digital/quizpayment?action=squizpay&dif=50000&data=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY4NDcxNjAxYzE0OWU0ZDI0ZWJmNTExNyIsImF1ZCI6InBlbmEiLCJpc3MiOiJwZW5hLWF1dGgtc2VydmljZSIsImlhdCI6MTc0OTQ4OTE1MywiZXhwIjoxNzU0NjczMTUzfQ.SqEXFSPzP3ugIscqLywGkjFJmFqx13zOtxGAjZQ6SzUw8w9ZXjE9uFn8VbLBMGPqeJbvcT2jRV2bB5qtqMy1T3aNuSZ9AZW0jY1hGvFB-bSrYguMV1yErLkR45SvdK2jGI6dg3p6LRqCHb2JMl8vur_KKaus3GiJlsTNNR0fhgI&userid=68471601c149e4d24ebf5117&from=AI&wayback=ai_27969 diff --git a/src/pages/Payment/useAutoPay.ts b/src/pages/Payment/useAutoPay.ts new file mode 100644 index 0000000..1136729 --- /dev/null +++ b/src/pages/Payment/useAutoPay.ts @@ -0,0 +1,109 @@ +import React, { useEffect, useState, useRef } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { cancelPayCartProcess, setNotEnoughMoneyAmount, startPayCartProcess } from "@root/stores/allTypesOfPurchases"; +import { useUserStore } from "@root/stores/user"; +import { setAction, setBackWay, setFromDomain, setAdditionalinformation } from "@root/stores/allTypesOfPurchases"; +import { Action, FromDomain } from "@root/model/autoPay"; + +export const useAutoPay = (): [string, React.Dispatch>] => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const hasProcessed = useRef(false); + + const userId = useUserStore(store => store.userId) + const [paymentValueField, setPaymentValueField] = useState("0"); + + useEffect(() => { + // Если уже обработали, не выполняем повторно + if (hasProcessed.current) { + console.log("useAutoPay - уже обработано, пропускаем"); + return; + } + + console.log("useAutoPay - useEffect запущен"); + + // Сначала извлекаем все параметры + let URLfromDomain = searchParams.get("fromdomain");//домен откуда произошёл запрос на финансовые работы + let URLaction = searchParams.get("action");//что мы, собсна, хотим: оплатить, пополнить, купить и создать квиз + let URLuserId = searchParams.get("userid");//тот кто начал всё это действо + const URLwayBack = searchParams.get("wayback") ?? "";//путь по которому нужно будет пройти после того как закончим манипуляции (без домена) + let URLmoneyDifferent = Number(searchParams.get("dif"));//сколько нужно деняк (в копейках) + const URLadditionalinformation = searchParams.get("additionalinformation") ?? "";//дополнительная информация + + console.log("useAutoPay - извлеченные параметры:"); + console.log("URLfromDomain:", URLfromDomain); + console.log("URLaction:", URLaction); + console.log("URLuserId:", URLuserId); + console.log("URLwayBack:", URLwayBack); + console.log("URLmoneyDifferent (копейки):", URLmoneyDifferent); + console.log("URLadditionalinformation:", URLadditionalinformation); + console.log("userId:", userId); + + // Если нет URL параметров, но мы уже на странице /payment, не делаем ничего + if (!URLfromDomain && !URLaction && !URLuserId && URLmoneyDifferent === 0) { + console.log("useAutoPay - нет URL параметров, но мы уже на /payment, пропускаем"); + hasProcessed.current = true; + return; + } + + //Анализ нужно ли вообще вмешательство этого хука + const condition1 = URLuserId !== null && URLuserId && URLuserId === userId; + const condition2 = URLfromDomain === "quiz" || URLfromDomain === "squiz" || URLfromDomain === "hub" || URLfromDomain === "shub" || URLfromDomain === "localhost:3000"; + const condition3 = URLaction === "topupwallet" || URLaction === "createquizcc" || URLaction === "buy"; + const condition4 = !Number.isNaN(URLmoneyDifferent); + + console.log("useAutoPay - проверка условий:"); + console.log("condition1 (userId совпадает):", condition1); + console.log("condition2 (fromDomain валидный):", condition2); + console.log("condition3 (action валидный):", condition3); + console.log("condition4 (moneyDifferent валидный):", condition4); + + if (condition1 && condition2 && condition3 && condition4) { + console.log("useAutoPay - условия выполнены, устанавливаем значения"); + + // Ставим флаг, что сайт в состоянии ожидания пополнения счёта для оплаты + startPayCartProcess(URLuserId); + + console.log("useAutoPay - устанавливаем fromDomain:", URLfromDomain); + setFromDomain(URLfromDomain as FromDomain); + + const processedBackWay = URLwayBack.startsWith('/') ? URLwayBack.slice(1) : URLwayBack; + console.log("useAutoPay - устанавливаем backWay:", processedBackWay); + setBackWay(processedBackWay); + + console.log("useAutoPay - устанавливаем action:", URLaction); + setAction(URLaction as Action); + + console.log("useAutoPay - устанавливаем additionalinformation:", URLadditionalinformation); + setAdditionalinformation(URLadditionalinformation); + + // URLmoneyDifferent в копейках, конвертируем в рубли для отображения + const rubles = URLmoneyDifferent / 100; + setPaymentValueField(rubles.toString()); + setNotEnoughMoneyAmount(URLmoneyDifferent); + + console.log("useAutoPay - установлено paymentValueField (рубли):", rubles.toString()); + console.log("useAutoPay - установлено backWay:", processedBackWay); + console.log("useAutoPay - установлено additionalinformation:", URLadditionalinformation); + + // Только после установки всех значений делаем навигацию + navigate("/payment", { replace: true }); + } else { + console.log("useAutoPay - условия не выполнены, отменяем процесс"); + console.log("Причины:"); + if (!condition1) console.log("- userId не совпадает"); + if (!condition2) console.log("- fromDomain не валидный:", URLfromDomain); + if (!condition3) console.log("- action не валидный:", URLaction); + if (!condition4) console.log("- moneyDifferent не валидный:", URLmoneyDifferent); + cancelPayCartProcess(); + } + + // Отмечаем, что обработали + hasProcessed.current = true; + + // Убираем cleanup функцию, так как она вызывается слишком рано + // Cleanup будет в Payment компоненте при размонтировании + }, [searchParams, userId, navigate]) + + return [paymentValueField, setPaymentValueField]; +} \ No newline at end of file diff --git a/src/pages/QuizPayment/QuizPayment.tsx b/src/pages/QuizPayment/QuizPayment.tsx deleted file mode 100644 index 3bba7f4..0000000 --- a/src/pages/QuizPayment/QuizPayment.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useEffect, useState } from "react"; -import { ApologyPage } from "../ApologyPage"; -import { useNavigate } from "react-router-dom"; -import { - clearAuthToken, - getMessageFromFetchError, - setAuthToken, - useUserAccountFetcher, - useUserFetcher, - getAuthToken, -} from "@frontend/kitui"; -import { - clearUserData, - setUser, - setUserAccount, - setUserId, - useUserStore, -} from "@root/stores/user"; -import { logout } from "@root/api/auth"; -import { clearCustomTariffs } from "@root/stores/customTariffs"; -import { clearTickets } from "@root/stores/tickets"; -import { setNotEnoughMoneyAmount } from "@stores/cart" - -const params = new URLSearchParams(window.location.search); -const action = params.get("action"); -const dif = params.get("dif"); -const token = params.get("data"); -const userId = params.get("userid"); -const wayback = params.get("wayback"); - -let first = true; - -export default function QuizPayment() { - const navigate = useNavigate(); - const [message, setMessage] = useState("Идёт загрузка"); - const user = useUserStore((state) => state.user); - - useEffect( - function redirectIfSignedIn() { - if (!first && user?._id === userId) { - let returnUrl = `/payment?action=${action}&dif=${dif}&user=${userId}` - if (wayback) returnUrl += `&wayback=${wayback}` - navigate(returnUrl, { - replace: true, - }); - } - }, - [navigate, user] - ); - - - if (first) { - navigate(`/quizpayment`, { - replace: true, - }); - try { - first = false; - - if (user?._id === userId) { - return; - } - - if (action && dif && token) { - (async () => { - if (getAuthToken()) { - clearAuthToken(); - clearUserData(); - clearCustomTariffs(); - clearTickets(); - setNotEnoughMoneyAmount(0) - await logout(); - } - - setAuthToken(token); - - // setAuthToken(data.data.accessToken) - setUserId(userId); - - // useUserFetcher({ - // url: process.env.REACT_APP_DOMAIN + `/user/${userId}`, - // userId, - // onNewUser: (user) => { - // setUser(user) - // navigate(`/payment?action=${action}&dif=${dif}`, { replace: true }) - - // }, - // onError: () => { }, - // }) - return; - })(); - } else { - var link = document.createElement("a"); - link.href = "https://quiz.pena.digital/tariffs"; - document.body.appendChild(link); - link.click(); - } - } catch (e) { - setMessage("Произошла ошибка"); - var link = document.createElement("a"); - link.href = "https://quiz.pena.digital/tariffs"; - document.body.appendChild(link); - link.click(); - } - } - - return ; -} diff --git a/src/pages/Support/SupportChat.tsx b/src/pages/Support/SupportChat.tsx index 199349e..9c60854 100644 --- a/src/pages/Support/SupportChat.tsx +++ b/src/pages/Support/SupportChat.tsx @@ -101,17 +101,8 @@ function SupportChat() { `/heruvym/v1.0.0/ticket?ticket=${ticketId}&Authorization=${token}`, onNewData: (ticketMessages) => { - console.log("SupportChat") - console.log("ticketMessages") - console.log(ticketMessages) const data = ticketMessages.filter(t => { if (typeof t === "object" && t !== null && "event" in t && t.event !== "ping") { - console.log("---------------------------------------------------") - console.log(t) - console.log(typeof t === "object") - console.log("event" in t) - console.log(t.event !== "ping") - console.log("---------------------------------------------------") return true } }) @@ -235,9 +226,6 @@ function SupportChat() { setDisableFileButton(false); }; - console.log("messages messmessagesmessagesmessages messages") - console.log(messages) - return ( 0 && isFileImage() ) { - console.log("message NEWNEWNENWNEW _WE__WE_W_EW_E_WENWNEWNENWEWNE") - console.log(message) return ( state.ticketsFetchState) const sortedTickets = tickets + .filter(ticket => !ticket?.top_message?.system) .sort(sortTicketsByUpdateTime) .slice( ticketApiPage * ticketsPerPage, diff --git a/src/pages/Tariffs/ModalRequestCreate.tsx b/src/pages/Tariffs/ModalRequestCreate.tsx new file mode 100644 index 0000000..251e5c1 --- /dev/null +++ b/src/pages/Tariffs/ModalRequestCreate.tsx @@ -0,0 +1,328 @@ +import { Box, Button, IconButton, Modal, Typography, useMediaQuery, useTheme } from "@mui/material" + +import { Form, Formik, useFormik } from "formik" +import CustomTextField from "@components/CustomTextField" +import InfoButton from "@components/InfoButton"; +import { sendContactFormRequest } from "@api/tariff" +import { TimePicker } from "@mui/x-date-pickers" +import moment, { Moment } from "moment" +import { enqueueSnackbar } from "notistack" +import { useState } from "react" +import CloseIcon from "@root/assets/Icons/CloseIcon"; +import { useRequestSquizCreate, RSCClose } from "@root/stores/requestSquizCreate" + + +interface Values { + contact: string; + dogiebusiness: string; + imagination: string; + name: string; + time?: Moment | null; +} +const initialValues: Values = { + contact: "",//phone number + // whoami: {}, + dogiebusiness: "", + imagination: "", + name: "", + time: null +}; + +interface FP { + title: string + desc?: string + placeholder: string + value: string + onChange: any + rows?: number +} + +const Field = ({ + title, + desc, + placeholder, + value, + onChange, + rows, +}: FP) => { + return ( + + {title} + {desc && {desc}} + + + ) +} + +export const ModalRequestCreate = () => { + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down(650)); + const [isSending, setIsSending] = useState(false) + const open = useRequestSquizCreate(store => store.isRSCOpen) + + if (isSending) return ( + { + RSCClose() + setIsSending(false) + }} + + > + + Спасибо за заявку! + С вами свяжутся в течение 24 часов + + + + + ) + return ( + + + + + + Заполните форму, чтобы оставить заявку на создание квиза + + + + + + + + { + if (values.contact.length < 8) return enqueueSnackbar("Пожалуйста, оставьте контактные данные") + const resp = await sendContactFormRequest({ + contact: values.contact, + whoami: JSON.stringify({ + dogiebusiness: values.dogiebusiness, + imagination: values.imagination, + name: values.name, + time: moment(values.time).format("hh:mm") + }) + }) + //@ts-ignore + if (resp[0]?.status === 200) { + enqueueSnackbar("Запрос успешно отправлен") + setIsSending(true) + } + if (resp[1]) { + //@ts-ignore + enqueueSnackbar(resp[1]) + } + }} + > + {({ values, isSubmitting, setFieldValue }) => (<> +
+ setFieldValue("name", target.value)} + + /> + setFieldValue("dogiebusiness", target.value)} + + /> + setFieldValue("contact", target.value)} + desc="(Telegram, WhatsApp, номер телефона)" + /> + setFieldValue("imagination", target.value)} + rows={2} + /> + + + Во сколько вам можно позвонить? + Москва (GMT+3) + + setFieldValue("time", e)} + ampm={false} + value={values.time} + views={['hours', 'minutes']} format="hh:mm" + sx={{ + border: "#c4c4c4 1px solid", + borderRadius: "4px", + "& .MuiInputBase-root": { + display: "flex" + } + }} + /> + + + + + + + + + )} +
+
+
+
+ ) +} diff --git a/src/pages/Tariffs/TariffCard.tsx b/src/pages/Tariffs/TariffCard.tsx index a5ba727..78195b3 100644 --- a/src/pages/Tariffs/TariffCard.tsx +++ b/src/pages/Tariffs/TariffCard.tsx @@ -1,7 +1,8 @@ -import { Box, Typography, Tooltip, SxProps, Theme, Button, Badge, useTheme, useMediaQuery, } from "@mui/material"; +import { Box, Typography, Tooltip, SxProps, Theme, Button, Badge, useTheme, useMediaQuery, IconButton, } from "@mui/material"; import { MouseEventHandler, ReactNode } from "react"; import { cardShadow } from "@root/utils/theme"; import { relative } from "path"; +import NotebookWithPencil from "@root/assets/Icons/NotebookWithPencil copy"; interface Props { icon: ReactNode; @@ -15,11 +16,12 @@ interface Props { text?: string; }; price?: ReactNode; + sendRequestToCreate?: () => void; } -export default function TariffCard({ icon, headerText, text, sx, price, buttonProps, discount }: Props) { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(600)); +export default function TariffCard({ icon, headerText, text, sx, price, buttonProps, discount, sendRequestToCreate }: Props) { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(600)); return ( )} - {/* {headerText}} placement="top"> */} - - {headerText} - + {/* {headerText}} placement="top"> */} + + {headerText} + {/* ( @@ -100,29 +102,67 @@ export default function TariffCard({ icon, headerText, text, sx, price, buttonPr ))} placement="top" > */} - - - {text} - - + + + {text} + + {/* */} - {buttonProps && ( - - )} + + {buttonProps && ( + + )} + {Boolean(sendRequestToCreate) && ( + + {isMobile ? + + + + : + + } + + )} +
); } diff --git a/src/pages/Tariffs/Tariffs.tsx b/src/pages/Tariffs/Tariffs.tsx index a74554a..bb8dda2 100644 --- a/src/pages/Tariffs/Tariffs.tsx +++ b/src/pages/Tariffs/Tariffs.tsx @@ -89,14 +89,14 @@ export default function Tariffs() { : diff --git a/src/pages/Tariffs/TariffsPage.tsx b/src/pages/Tariffs/TariffsPage.tsx index 6ce564b..03386a0 100644 --- a/src/pages/Tariffs/TariffsPage.tsx +++ b/src/pages/Tariffs/TariffsPage.tsx @@ -28,8 +28,10 @@ import TariffCard from "./TariffCard"; import { useDiscounts } from "@root/api/price"; import { Select } from "@components/Select"; import { Tabs } from "@components/Tabs"; +import { useRequestSquizCreate, RSCOpen, RSCClose } from "@root/stores/requestSquizCreate" -const subPages = ["Базовый тариф PenaQuiz", 'Убрать логотип "PenaQuiz"']; +const subPagesTime = ["Базовый тариф PenaQuiz", 'Убрать логотип "PenaQuiz"']; +const subPagesVolume = ["Заявки квиз", 'Заказать создание квиза']; const StepperText: Record = { volume: "Тарифы на объём", @@ -43,10 +45,12 @@ function TariffPage() { const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const location = useLocation(); const tariffs = useTariffStore((state) => state.tariffs); - const [selectedItem, setSelectedItem] = useState(0); + const [selectedItemTime, setSelectedItemTime] = useState(0); + const [selectedItemVolume, setSelectedItemVolume] = useState(0); const purchasesAmount = useUserStore((state) => state.userAccount?.wallet.spent) ?? 0; const userId = useUserStore((state) => state.user?._id) ?? ""; + const userPrivilegies = useUserStore(store => store.quizUserAccount?.privileges); const discounts = useDiscounts(userId); const isUserNko = useUserStore((state) => state.userAccount?.status) === "nko"; @@ -68,25 +72,14 @@ function TariffPage() { } const filteredTariffs = tariffs.filter((tariff) => { - if (tariff.privileges[0] === undefined) return false; - if ( - (tariff.privileges[0].type === "day") === (unit === "time") && - !tariff.isDeleted && - !tariff.isCustom - ) { - if ( - ((selectedItem === 0 && unit === "time") || unit !== "time") && - tariff.privileges[0].serviceKey === "squiz" && - tariff.privileges[0].privilegeId !== "squizHideBadge" - ) - return true; + if (tariff.privileges[0] === undefined || tariff.isDeleted || tariff.isCustom) return false; + if (unit === "time") { + if (selectedItemTime === 0 && tariff.privileges[0].privilegeId === "quizUnlimTime") return true + if (selectedItemTime === 1 && tariff.privileges[0].privilegeId === "squizHideBadge") return true } - if ( - selectedItem === 1 && - unit === "time" && - tariff.privileges[0].privilegeId === "squizHideBadge" - ) { - return true; + if (unit === "volume") { + if (selectedItemVolume === 0 && tariff.privileges[0].privilegeId === "quizCnt") return true + if (selectedItemVolume === 1 && tariff.privileges[0].privilegeId === "quizManual") return true } return false; }); @@ -95,6 +88,7 @@ function TariffPage() { filteredTariffs: Tariff[], addFreeTariff = false ) => { + const isCC = userPrivilegies?.quizManual?.amount !== undefined && userPrivilegies?.quizManual?.amount > 0 && unit === "volume" && selectedItemVolume === 1 const tariffElements = filteredTariffs .filter((tariff) => tariff.privileges.length > 0) .map((tariff, index) => { @@ -104,7 +98,7 @@ function TariffPage() { purchasesAmount, currentTariffs ?? [], isUserNko, - userId + userId, ); return ( @@ -133,6 +127,7 @@ function TariffPage() { text: "Выбрать", onClick: () => handleTariffItemClick(tariff._id), }} + sendRequestToCreate={isCC ? RSCOpen : undefined} headerText={tariff.name} text={tariff.description || ""} price={ @@ -206,15 +201,32 @@ function TariffPage() { <> {isMobile ? ( + ) : ( + )} @@ -231,7 +243,7 @@ function TariffPage() { }))`, }} > - {createTariffElements(filteredTariffs, true)} + {createTariffElements(filteredTariffs, unit === "time" || (unit === "volume" && selectedItemVolume !== 1))} {/*{recentlyPurchased.length > 0 && (*/} {/* <>*/} diff --git a/src/stores/allTypesOfPurchases.ts b/src/stores/allTypesOfPurchases.ts new file mode 100644 index 0000000..0158edb --- /dev/null +++ b/src/stores/allTypesOfPurchases.ts @@ -0,0 +1,67 @@ +import moment from "moment"; +import { create } from "zustand"; +import { devtools, persist } from "zustand/middleware"; +import type { Action, FromDomain } from "@root/model/autoPay"; + +interface CartStore { + notEnoughMoneyAmount: number; + siteReadyPayCart: Record | null; + fromDomain: FromDomain | null; + backWay: string; + action: Action | null; + additionalinformation: string; +} +const initialState: CartStore = { + notEnoughMoneyAmount: 0, + siteReadyPayCart: null, + fromDomain: null, + backWay: "", + action: null, + additionalinformation: "" +} + +//Была попытка оплатить корзину. Тут записанна недостающая сумма. +export const allTypesOfPurchases = create()( + devtools( + persist( + () => initialState, + { + name: 'siteReadyPayCart', // Ключ для localStorage + partialize: (state) => ({ + // Сохраняем ТОЛЬКО siteReadyPayCart + siteReadyPayCart: state.siteReadyPayCart + }), + } + ), + { + name: "cartStore", + enabled: process.env.NODE_ENV === "development", + } + ) + ); + +export const setSiteReadyPayCart = (flag: Record | null) => allTypesOfPurchases.setState({ siteReadyPayCart: flag }); +export const setNotEnoughMoneyAmount = (amount: number) => allTypesOfPurchases.setState({ notEnoughMoneyAmount: amount }); +export const setFromDomain = (fromDomain: FromDomain) => allTypesOfPurchases.setState({ fromDomain }); +export const setBackWay = (backWay: string) => allTypesOfPurchases.setState({ backWay }); +export const setAction = (action: Action) => allTypesOfPurchases.setState({ action }); +export const setAdditionalinformation = (additionalinformation: string) => allTypesOfPurchases.setState({ additionalinformation }); + +export const startPayCartProcess = (userId: string) => setSiteReadyPayCart({ [userId]: moment().add(20, 'minutes').format("X") }); +export const cancelPayCartProcess = () => { + setSiteReadyPayCart(null); + allTypesOfPurchases.setState({ + fromDomain: null, + backWay: "", + action: null + }) +}; + + +export const calcTimeOfReadyPayCart = (deadline: string) => { + const ready = Number(deadline) > Number(moment().format("X")) + if (!ready) { + cancelPayCartProcess() + } + return ready +} diff --git a/src/stores/cart.ts b/src/stores/cart.ts deleted file mode 100644 index 0a6b120..0000000 --- a/src/stores/cart.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { create } from "zustand"; -import { devtools } from "zustand/middleware"; - -interface CartStore { - notEnoughMoneyAmount: number; -} - -export const useCartStore = create()( - devtools( - (get, set) => ({ - notEnoughMoneyAmount: 0, - }), - { - name: "Cart", - enabled: process.env.NODE_ENV === "development", - trace: true, - actionsBlacklist: "rejected", - } - ) -); - -export const setNotEnoughMoneyAmount = (amount: number) => useCartStore.setState({ notEnoughMoneyAmount: amount }); diff --git a/src/stores/diffMoney.ts b/src/stores/diffMoney.ts deleted file mode 100644 index 2e3a49a..0000000 --- a/src/stores/diffMoney.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { HistoryRecord, HistoryRecord2 } from "@root/api/history"; -import { create } from "zustand"; -import { devtools, persist } from "zustand/middleware"; - - -type DiffMoneyType = { - diffMoney: number; - setNewDiff: (diff: number) => void -}; - -export const useDiffMoney = create((set) => ({ - diffMoney: 0, - setNewDiff: (diff: number) => set({diffMoney: diff}) - })); diff --git a/src/stores/requestSquizCreate.ts b/src/stores/requestSquizCreate.ts new file mode 100644 index 0000000..114c50e --- /dev/null +++ b/src/stores/requestSquizCreate.ts @@ -0,0 +1,25 @@ +import { Privilege } from "@frontend/kitui"; +import { create } from "zustand" +import { devtools } from "zustand/middleware" + + +interface RequestSquizCreate { + isRSCOpen: boolean; +} + +const initialState: RequestSquizCreate = { + isRSCOpen: false +} + +export const useRequestSquizCreate = create()( + devtools( + (get, set) => initialState, + { + name: "Privileges", + enabled: process.env.NODE_ENV === "development", + } + ) +) + +export const RSCOpen = () => useRequestSquizCreate.setState({ isRSCOpen: true }) +export const RSCClose = () => useRequestSquizCreate.setState({ isRSCOpen: false }) \ No newline at end of file diff --git a/src/stores/tickets.ts b/src/stores/tickets.ts index 51a3fcf..a4e48fd 100644 --- a/src/stores/tickets.ts +++ b/src/stores/tickets.ts @@ -44,7 +44,7 @@ const initialState: TicketStore = { ticketCount: 0, tickets: [], apiPage: 0, - ticketsPerPage: 10, + ticketsPerPage: 100, ticketsFetchState: "idle", authData: initAuthData, unauthData: initAuthData, diff --git a/src/stores/user.ts b/src/stores/user.ts index c37541a..848fc70 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -14,23 +14,42 @@ import { patchUser } from "@root/api/user" import { UserAccountSettingsFieldStatus, VerificationStatus } from "@root/model/account" import { patchCurrency, deleteCart, patchCart } from "@root/api/cart" import { User, UserAccount, UserName, getInitials, patchUserAccount } from "@frontend/kitui" -import { setNotEnoughMoneyAmount } from "./cart"; +import { cancelPayCartProcess, setNotEnoughMoneyAmount, setSiteReadyPayCart, allTypesOfPurchases } from "./allTypesOfPurchases"; + +type Privilege = { + amount: number; + created_at: string; + id: string; + privilege_id: string; + privilege_name: string; + }; interface UserStore { - userId: string | null; - user: User | null; - userAccount: UserAccount | null; - settingsFields: UserSettingsFieldStatus & UserAccountSettingsFieldStatus & { hasError: boolean }; - verificationStatus: VerificationStatus; - verificationType: "juridical" | "nko"; - isDocumentsDialogOpen: boolean; - dialogType: "juridical" | "nko" | ""; - documents: UserDocuments; - documentsUrl: UserDocumentsUrl; - comment: string; - initials: string; + userId: string | null; + user: User | null; + userAccount: UserAccount | null; + settingsFields: UserSettingsFieldStatus & UserAccountSettingsFieldStatus & { hasError: boolean }; + verificationStatus: VerificationStatus; + verificationType: "juridical" | "nko"; + isDocumentsDialogOpen: boolean; + dialogType: "juridical" | "nko" | ""; + documents: UserDocuments; + documentsUrl: UserDocumentsUrl; + comment: string; + initials: string; + quizUserAccount: OriginalUserSquizAccount | null; } + +export type OriginalUserSquizAccount = { + created_at: string; + deleted: boolean; + email: string; + id: string; + privileges: Record; + privilege_name: string; + }; + const defaultFieldValues = { value: "", error: null, @@ -58,6 +77,7 @@ const initialState: UserStore = { userId: null, user: null, userAccount: null, + quizUserAccount: null, settingsFields: { ...defaultFields }, verificationStatus: VerificationStatus.NOT_VERIFICATED, verificationType: "juridical", @@ -103,10 +123,22 @@ export const useUserStore = create()( export const setVerificationStatus = (verificationStatus: VerificationStatus) => useUserStore.setState({ verificationStatus }) -export const setVerificationType = (verificationType: "nko" | "org") => +export const setVerificationType = (verificationType: UserAccount["status"]) => useUserStore.setState({ verificationType: verificationType === "org" ? "juridical" : "nko", }) +export const setUserStatus = (status: UserAccount["status"]) => + useUserStore.setState( + produce((state) => { + if (state.userAccount !== null) state.userAccount.status = status + }) + ) +export const setWallet = (wallet: UserAccount["wallet"]) => + useUserStore.setState( + produce((state) => { + if (state.userAccount !== null) state.userAccount.wallet = wallet + }) + ) export const setUserId = (userId: string | null) => useUserStore.setState({ userId }) export const setUser = (user: User) => @@ -114,7 +146,7 @@ export const setUser = (user: User) => produce((state) => { state.user = user - state.settingsFields.email.value = user?.email ?? "" + state.settingsFields.email.value = user?.email || user?.login || "" state.settingsFields.phoneNumber.value = user?.phoneNumber ?? "" state.settingsFields.password.value = "" }) @@ -139,12 +171,36 @@ export const setUserAccount = (user: UserAccount) => } ) -export const setCart = (cart: string[]) => +export const setQuizUserAccount = (quizUserAccount: OriginalUserSquizAccount) => useUserStore.setState({ quizUserAccount }); + +export const setNewNames = (name: UserName) => useUserStore.setState( produce((state) => { - if (state.userAccount) state.userAccount.cart = cart + state.settingsFields.firstname.value = name.firstname ?? "" + state.settingsFields.secondname.value = name.secondname ?? "" + state.settingsFields.middlename.value = name.middlename ?? "" + state.settingsFields.orgname.value = name.orgname ?? "" + }), + false, + { + type: "setNewNames", + payload: name, + } + ) + +export const setCart = (cart: string[]) => { + + useUserStore.setState( + produce((state) => { + if (state.userAccount) { + state.userAccount.cart = cart + } }) ) + //Изменение корзины ведёт к отмене этих 2 параметров всегда + setNotEnoughMoneyAmount(0) + cancelPayCartProcess() +} export const setComment = (comment: string) => useUserStore.setState({ comment }) @@ -216,7 +272,7 @@ export const setUploadedDocument = (type: UserDocumentTypes, fileName: string, u }) ) -export const setSettingsField = (fieldName: UserSettingsField | keyof UserName, value: string) => +export const setSettingsField = (fieldName: UserSettingsField | keyof UserName, value: string) => useUserStore.setState( produce((state) => { if (!state.settingsFields) return @@ -247,15 +303,15 @@ export const sendUserData = async () => { if (!state.settingsFields) return const isPatchingUser = - state.settingsFields.email.touched || - state.settingsFields.password.touched || - state.settingsFields.phoneNumber.touched + state.settingsFields.email.touched || + state.settingsFields.password.touched || + state.settingsFields.phoneNumber.touched const isPatchingUserAccount = - state.settingsFields.firstname.touched || - state.settingsFields.secondname.touched || - state.settingsFields.middlename.touched || - state.settingsFields.orgname.touched + state.settingsFields.firstname.touched || + state.settingsFields.secondname.touched || + state.settingsFields.middlename.touched || + state.settingsFields.orgname.touched const userPayload: PatchUserRequest = {} @@ -273,7 +329,7 @@ export const sendUserData = async () => { await Promise.all([ isPatchingUser && patchUser(userPayload).then(([user]) => user && setUser(user)), - isPatchingUserAccount && patchUserAccount(userAccountPayload, "1.0.1").then(setUserAccount), + isPatchingUserAccount && patchUserAccount(userAccountPayload, "v1.0.1").then(setUserAccount), ]) } @@ -283,11 +339,11 @@ export const addTariffToCart = async (tariffId: string) => { if (patchCartError === undefined) { setCart(patchCartResponse) } - return({patchCartResponse, patchCartError}) + return ({ patchCartResponse, patchCartError }) } export const removeTariffFromCart = async (tariffId: string) => { - setNotEnoughMoneyAmount(0); + setNotEnoughMoneyAmount(0); const [deleteCartResponse, deleteCartError] = await deleteCart(tariffId) if (!deleteCartError) { diff --git a/src/useUserAccountFetcher.ts b/src/useUserAccountFetcher.ts new file mode 100644 index 0000000..7e0841f --- /dev/null +++ b/src/useUserAccountFetcher.ts @@ -0,0 +1,60 @@ +import { useEffect, useLayoutEffect, useRef } from "react"; +import { createUserAccount, devlog, getAuthToken, setAuthToken } from "@frontend/kitui"; +import { isAxiosError } from "axios"; + +import { makeRequest } from "@frontend/kitui"; + +import type { UserAccount } from "@frontend/kitui"; + +export const useUserAccountFetcher = ({ + onError, + onNewUserAccount, + url, + userId, +}: { + url: string; + userId: string | null; + onNewUserAccount: (response: T) => void; + onError?: (error: any) => void; +}) => { + const onNewUserAccountRef = useRef(onNewUserAccount); + const onErrorRef = useRef(onError); + useLayoutEffect(() => { + onNewUserAccountRef.current = onNewUserAccount; + onErrorRef.current = onError; + }, [onError, onNewUserAccount]); + useEffect(() => { + if (!userId) return; + const controller = new AbortController(); + makeRequest({ + url, + contentType: true, + method: "GET", + useToken: true, + withCredentials: false, + signal: controller.signal, + }) + .then((result) => { + devlog("User account", result); + onNewUserAccountRef.current(result); + }) + .catch((error) => { + devlog("Error fetching user account", error); + if (isAxiosError(error) && error.response?.status === 404) { + createUserAccount(controller.signal, url.replace("get", "create"), "v1.0.1") + .then((result) => { + devlog("Created user account", result); + onNewUserAccountRef.current(result as T); + }) + .catch((error) => { + if (error.response?.status === 409) return; + devlog("Error creating user account", error); + onErrorRef.current?.(error); + }); + } else { + onErrorRef.current?.(error); + } + }); + return () => controller.abort(); + }, [url, userId]); +}; diff --git a/src/utils/hooks/usePipeSubscriber.ts b/src/utils/hooks/usePipeSubscriber.ts new file mode 100644 index 0000000..bccfdf7 --- /dev/null +++ b/src/utils/hooks/usePipeSubscriber.ts @@ -0,0 +1,66 @@ +import { Ticket, UserAccount, UserName, getAuthToken, useSSESubscription } from "@frontend/kitui" +import { setCart, setNewNames, setUserStatus, setWallet, useUserStore } from "@root/stores/user"; +import { useSSETab } from "./useSSETab"; +import { cancelPayCartProcess } from "@root/stores/allTypesOfPurchases"; + +type Ping = [{ event: "ping" }] + +type SomeChange = [{ + "id": UserAccount["_id"], + "userId": UserAccount["userId"], + "cart": UserAccount["cart"], + "wallet": { + "cash": UserAccount["wallet"]["cash"], + "currency": UserAccount["wallet"]["currency"], + "spent": UserAccount["wallet"]["spent"], + "purchasesAmount": UserAccount["wallet"]["purchasesAmount"], + "money": UserAccount["wallet"]["money"], + "lastPaymentId": string; + }, + "name": UserName, + "status": UserAccount["status"], + "isDeleted": UserAccount["isDeleted"], + "createdAt": UserAccount["createdAt"]; + "updatedAt": UserAccount["updatedAt"]; + "from": string; + "partner": string; +}] + +type PipeMessage = Ping | SomeChange + +export const usePipeSubscriber = () => { + const token = getAuthToken(); + const userId = useUserStore((state) => state.userId); + const { isActiveSSETab, updateSSEValue } = useSSETab("pipe"); + + useSSESubscription({ + enabled: Boolean(token) && Boolean(userId) && isActiveSSETab, + url: + process.env.REACT_APP_DOMAIN + + `/customer/v1.0.1/account/pipe?Authorization=${token}`, + onNewData: (data) => { + let message = data[0] as PipeMessage + updateSSEValue(message) + + //Пропускаем пингование + if ('event' in message && message.event === "ping") return + + + if ('cart' in message) { + setCart(message.cart as string[]) + cancelPayCartProcess() + } + else if ('wallet' in message) { + setWallet(message.wallet as UserAccount["wallet"]) + cancelPayCartProcess() + } + else if ('name' in message) { + setNewNames(message.name as UserName) + } + else if ('status' in message) { + setUserStatus(message.status as UserAccount["status"]) + } + }, + marker: "pipe", + }); +} \ No newline at end of file diff --git a/src/utils/hooks/useReauthorization.ts b/src/utils/hooks/useReauthorization.ts new file mode 100644 index 0000000..c5cbefe --- /dev/null +++ b/src/utils/hooks/useReauthorization.ts @@ -0,0 +1,105 @@ +import { clearAuthToken, getAuthToken, setAuthToken } from '@frontend/kitui'; +import { logout } from '@root/api/auth'; +import { setNotEnoughMoneyAmount } from '@root/stores/allTypesOfPurchases'; +import { clearCustomTariffs } from '@root/stores/customTariffs'; +import { clearTickets } from '@root/stores/tickets'; +import { clearUserData, setUserId, useUserStore } from '@root/stores/user'; +import { useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { clearUserSession, logoutAndRedirect } from "@root/utils/logout"; + +interface QuizAuthParams { + action?: string; + dif?: string; + data?: string; + userid?: string; + wayback?: string; +} + +export const useReauthorization = () => { + const userId = useUserStore(store => store.userId); + const user = useUserStore(store => store.user); + const { search } = useLocation(); + const navigate = useNavigate(); + const [isProcessing, setIsProcessing] = useState(false); + + useEffect(() => { + // Этот эффект сработает при каждом изменении query-параметров + const params = new URLSearchParams(search); + + // Обработка старых параметров (userid, sec) + const URLuserId = params.get('userid'); + const URLtoken = params.get('sec'); + + // Обработка новых параметров авторизации + const quizParams: QuizAuthParams = { + action: params.get("action") || undefined, + dif: params.get("dif") || undefined, + data: params.get("data") || undefined, + userid: params.get("userid") || undefined, + wayback: params.get("wayback") || undefined, + }; + + const { action, dif, data: token, userid: quizUserId, wayback } = quizParams; + + // Если есть новые параметры авторизации, обрабатываем их + if (action && dif && token && quizUserId) { + // Если пользователь уже авторизован и это тот же пользователь, перенаправляем на payment + if (user?._id === quizUserId) { + let returnUrl = `/payment?action=${action}&dif=${dif}&user=${quizUserId}`; + if (wayback) returnUrl += `&wayback=${wayback}`; + navigate(returnUrl, { replace: true }); + return; + } + + // Если уже обрабатываем авторизацию, не запускаем повторно + if (isProcessing) { + return; + } + + setIsProcessing(true); + + const processQuizAuth = async () => { + try { + // Если есть текущий токен, очищаем данные + if (getAuthToken()) { + logoutAndRedirect(navigate); + await logout(); + } + + // Устанавливаем новый токен и ID пользователя + setAuthToken(token); + setUserId(quizUserId); + } catch (error) { + console.error("Ошибка авторизации:", error); + + // Перенаправляем на внешний сайт в случае ошибки + const link = document.createElement("a"); + link.href = "https://quiz.pena.digital/tariffs"; + document.body.appendChild(link); + link.click(); + } finally { + setIsProcessing(false); + } + }; + + processQuizAuth(); + return; + } + + // Обработка старых параметров (userid, sec) + if (URLuserId !== userId && URLtoken) { + // Если есть токен в URL, устанавливаем его + // Очищаем данные только если токен действительно изменился + if (getAuthToken() !== URLtoken) { + clearUserSession(); + // Не вызываем logout() чтобы избежать перенаправления + } + setAuthToken(URLtoken); + } + }, [search, userId, user?._id, navigate, isProcessing]); + + return { + isProcessing, + }; +}; \ No newline at end of file diff --git a/src/utils/hooks/useSSETab.ts b/src/utils/hooks/useSSETab.ts index 6ff9833..8f71636 100644 --- a/src/utils/hooks/useSSETab.ts +++ b/src/utils/hooks/useSSETab.ts @@ -74,6 +74,7 @@ export const useSSETab = ( }; const setOpenTime = () => { + //Время установлено - пропускаем if (openTimeSetted) { return; } diff --git a/src/utils/hooks/useTryBuy.ts b/src/utils/hooks/useTryBuy.ts new file mode 100644 index 0000000..cc1f2d9 --- /dev/null +++ b/src/utils/hooks/useTryBuy.ts @@ -0,0 +1,29 @@ +import { payCart } from "@root/api/cart"; +import { allTypesOfPurchases, calcTimeOfReadyPayCart, cancelPayCartProcess } from "@root/stores/allTypesOfPurchases" +import { useUserStore } from "@root/stores/user"; +import { enqueueSnackbar } from "notistack"; +import { useEffect } from "react" + +export const useTryBuy = () => { + const userId = useUserStore(store => store.userId) + const userAccount = useUserStore(state => state.userAccount); + const siteReadyPayCart = allTypesOfPurchases(state => state.siteReadyPayCart); + + useEffect(() => { + if (userId !== null && siteReadyPayCart !== null && siteReadyPayCart[userId] !== undefined) { + const deadline = siteReadyPayCart[userId] + if (calcTimeOfReadyPayCart(deadline)) { + + //Время ещё не вышло. У нас стоит флаг покупать корзину если время не вышло. + (async () => { + const [, payCartError] = await payCart(); + + if (!payCartError) { + enqueueSnackbar("Товары успешно приобретены") + cancelPayCartProcess() + } + })() + } + } + }, [userAccount, userId, siteReadyPayCart]) +} \ No newline at end of file diff --git a/src/utils/jsonToFormdata.ts b/src/utils/jsonToFormdata.ts index 3267290..55f672f 100644 --- a/src/utils/jsonToFormdata.ts +++ b/src/utils/jsonToFormdata.ts @@ -1,4 +1,5 @@ import type { Attachment } from "@root/model/attachment" +import { transliterate } from 'transliteration'; type KeyValue = { [key: string]: T; @@ -24,11 +25,13 @@ export const jsonToFormdata = ( json: KeyValue ): FormData => { const formData = new FormData() + if (json.egrule !== undefined) delete json.egrule for (const key in json) { if (!key) continue const value = json[key] + if (typeof value !== "string") value.name = transliterate(value.name.replace(/\s/g, '_')) if (typeof value !== "string") { formData.append(key, convertBinaryStringToFile(value)) @@ -38,6 +41,5 @@ export const jsonToFormdata = ( formData.append(key, value) } - return formData } diff --git a/src/utils/logout.ts b/src/utils/logout.ts new file mode 100644 index 0000000..bc0350c --- /dev/null +++ b/src/utils/logout.ts @@ -0,0 +1,25 @@ +import { clearAuthToken } from "@frontend/kitui"; +import { clearUserData } from "@root/stores/user"; +import { clearCustomTariffs } from "@root/stores/customTariffs"; +import { clearTickets } from "@root/stores/tickets"; +import { setNotEnoughMoneyAmount } from "@root/stores/allTypesOfPurchases"; +import { NavigateFunction } from "react-router-dom"; + +/** + * Очищает все пользовательские данные (без редиректа) + */ +export function clearUserSession() { + clearAuthToken(); + clearUserData(); + clearCustomTariffs(); + clearTickets(); + setNotEnoughMoneyAmount(0); +} + +/** + * Выполняет полный логаут пользователя и редиректит на главную страницу + */ +export function logoutAndRedirect(navigate: NavigateFunction) { + clearUserSession(); + navigate("/"); +} \ No newline at end of file diff --git a/src/utils/routes/ProtectedRoute.tsx b/src/utils/routes/ProtectedRoute.tsx index 9c269d2..9ed892d 100644 --- a/src/utils/routes/ProtectedRoute.tsx +++ b/src/utils/routes/ProtectedRoute.tsx @@ -1,8 +1,9 @@ -import { Navigate, Outlet } from "react-router-dom" +import { Navigate, Outlet, useLocation } from "react-router-dom" import { useUserStore } from "@root/stores/user" export default function PrivateRoute() { + const location = useLocation() const user = useUserStore(state => state.user) return user ? : diff --git a/yarn.lock b/yarn.lock index d13bbba..3b1b8b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1059,6 +1059,11 @@ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.1.tgz#9fce313d12c9a77507f264de74626e87fd0dc541" integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog== +"@babel/runtime@^7.25.7", "@babel/runtime@^7.27.6": + version "7.27.6" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6" + integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q== + "@babel/template@^7.27.1", "@babel/template@^7.3.3": version "7.27.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" @@ -1516,10 +1521,10 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== -"@frontend/kitui@^1.0.108": - version "1.0.108" - resolved "http://gitea.pena/api/packages/skeris/npm/%40frontend%2Fkitui/-/1.0.108/kitui-1.0.108.tgz#1bb609dfe07668b6fd9a7b8618c229e0bb609f1e" - integrity sha512-4DiF7GHX0RbBMZpFioclc3B87N+HrGLv1B3DveUCdHzukfxvFXyEKnRZQ4wYljO2A3FLSD9+4Dr6cSuZYw95OQ== +"@frontend/kitui@^1.0.110": + version "1.0.110" + resolved "http://gitea.pena/api/packages/skeris/npm/%40frontend%2Fkitui/-/1.0.110/kitui-1.0.110.tgz#0c0a968293338537a2811e7761f8efe933893573" + integrity sha512-XOCev5zNtNZ8fu3IfK6oFNOqT8lE9jlmUX1kQ3OO+H30/LBpnBrww9nV/aHV2TEm0wYXdRMvaEtU6VOb72sDdg== dependencies: immer "^10.0.2" reconnecting-eventsource "^1.6.2" @@ -2104,11 +2109,30 @@ csstype "^3.1.3" prop-types "^15.8.1" +"@mui/types@^7.4.4": + version "7.4.4" + resolved "https://registry.npmjs.org/@mui/types/-/types-7.4.4.tgz#0c5cd56905231e27096b41d096f1c948c26bdd5d" + integrity sha512-p63yhbX52MO/ajXC7hDHJA5yjzJekvWD3q4YDLl1rSg+OXLczMYPvTuSuviPRCgRX8+E42RXz1D/dz9SxPSlWg== + dependencies: + "@babel/runtime" "^7.27.6" + "@mui/types@~7.2.15": version "7.2.24" resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.24.tgz#5eff63129d9c29d80bbf2d2e561bd0690314dec2" integrity sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw== +"@mui/utils@^5.16.6 || ^6.0.0 || ^7.0.0": + version "7.2.0" + resolved "https://registry.npmjs.org/@mui/utils/-/utils-7.2.0.tgz#31062697b41aa8ea8ef04e3d3fadca1dec3e1de1" + integrity sha512-O0i1GQL6MDzhKdy9iAu5Yr0Sz1wZjROH1o3aoztuivdCXqEeQYnEjTDiRLGuFxI9zrUbTHBwobMyQH5sNtyacw== + dependencies: + "@babel/runtime" "^7.27.6" + "@mui/types" "^7.4.4" + "@types/prop-types" "^15.7.15" + clsx "^2.1.1" + prop-types "^15.8.1" + react-is "^19.1.0" + "@mui/utils@^5.17.1": version "5.17.1" resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.17.1.tgz#72ba4ffa79f7bdf69d67458139390f18484b6e6b" @@ -2121,6 +2145,27 @@ prop-types "^15.8.1" react-is "^19.0.0" +"@mui/x-date-pickers@^7.13.0": + version "7.29.4" + resolved "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.29.4.tgz#b8808cb8e28c1d4e528b37b336effc8074e65faf" + integrity sha512-wJ3tsqk/y6dp+mXGtT9czciAMEO5Zr3IIAHg9x6IL0Eqanqy0N3chbmQQZv3iq0m2qUpQDLvZ4utZBUTJdjNzw== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0" + "@mui/x-internals" "7.29.0" + "@types/react-transition-group" "^4.4.11" + clsx "^2.1.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + +"@mui/x-internals@7.29.0": + version "7.29.0" + resolved "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.29.0.tgz#1f353b697ed1bf5594ac549556ade2e6841f4bf5" + integrity sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0" + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -2807,6 +2852,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== +"@types/prop-types@^15.7.15": + version "15.7.15" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" + integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== + "@types/q@^1.5.1": version "1.5.8" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.8.tgz#95f6c6a08f2ad868ba230ead1d2d7f7be3db3837" @@ -2834,7 +2884,7 @@ dependencies: "@types/react" "*" -"@types/react-transition-group@^4.4.10": +"@types/react-transition-group@^4.4.10", "@types/react-transition-group@^4.4.11": version "4.4.12" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== @@ -8903,6 +8953,11 @@ mlly@^1.7.3, mlly@^1.7.4: pkg-types "^1.3.0" ufo "^1.5.4" +moment@^2.30.1: + version "2.30.1" + resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -10344,7 +10399,7 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-is@^19.0.0: +react-is@^19.0.0, react-is@^19.1.0: version "19.1.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.1.0.tgz#805bce321546b7e14c084989c77022351bbdd11b" integrity sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg== @@ -11854,6 +11909,13 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +transliteration@^2.3.5: + version "2.3.5" + resolved "https://registry.npmjs.org/transliteration/-/transliteration-2.3.5.tgz#8f92309575f69e4a8a525dab4ff705ebcf961c45" + integrity sha512-HAGI4Lq4Q9dZ3Utu2phaWgtm3vB6PkLUFqWAScg/UW+1eZ/Tg6Exo4oC0/3VUol/w4BlefLhUUSVBr/9/ZGQOw== + dependencies: + yargs "^17.5.1" + tryer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" @@ -12893,9 +12955,9 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.3.1: +yargs@^17.3.1, yargs@^17.5.1: version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1"