fix payment
All checks were successful
Deploy / CreateImage (push) Successful in 8m40s
Deploy / DeployService (push) Successful in 22s

This commit is contained in:
Nastya 2025-07-18 12:47:30 +03:00
parent e44c64165a
commit 6169e03d9a
3 changed files with 266 additions and 156 deletions

@ -71,6 +71,10 @@ const App = () => {
const location = useLocation(); const location = useLocation();
const userId = useUserStore(state => state.userId); const userId = useUserStore(state => state.userId);
const navigate = useNavigate(); const navigate = useNavigate();
// Используем обновленный хук useReauthorization для обработки авторизации
const { isProcessing } = useReauthorization();
useUserFetcher({ useUserFetcher({
url: process.env.REACT_APP_DOMAIN + `/user/${userId}`, url: process.env.REACT_APP_DOMAIN + `/user/${userId}`,
userId, userId,
@ -188,6 +192,7 @@ const App = () => {
/> />
<Route path={"/image/:srcImage"} element={<ChatImageNewWindow />} /> <Route path={"/image/:srcImage"} element={<ChatImageNewWindow />} />
<Route element={<PrivateRoute />}> <Route element={<PrivateRoute />}>
<Route element={<ProtectedLayout />}> <Route element={<ProtectedLayout />}>
<Route path="/tariffs" element={<Tariffs />} /> <Route path="/tariffs" element={<Tariffs />} />

@ -8,6 +8,7 @@ import {
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
CircularProgress,
} from "@mui/material"; } from "@mui/material";
import { activatePromocode } from "@root/api/promocode"; import { activatePromocode } from "@root/api/promocode";
import { sendPayment, sendRSPayment } from "@root/api/wallet"; import { sendPayment, sendRSPayment } from "@root/api/wallet";
@ -31,6 +32,7 @@ import { WarnModal } from "./WarnModal";
import { mutate } from "swr"; import { mutate } from "swr";
import { allTypesOfPurchases } from "@root/stores/allTypesOfPurchases"; import { allTypesOfPurchases } from "@root/stores/allTypesOfPurchases";
import { useAutoPay } from "@root/utils/hooks/useAutoPay"; import { useAutoPay } from "@root/utils/hooks/useAutoPay";
import { useReauthorization } from "@root/utils/hooks/useReauthorization";
type PaymentMethod = { type PaymentMethod = {
label: string; label: string;
@ -61,6 +63,8 @@ export default function Payment() {
const navigate = useNavigate(); const navigate = useNavigate();
const handleCustomBackNavigation = useHistoryTracker(); const handleCustomBackNavigation = useHistoryTracker();
// Используем обновленный хук useReauthorization для обработки авторизации
const { isProcessing } = useReauthorization();
//Логика сбора данных из урла "чё мы ваще пришли на эту страницу" //Логика сбора данных из урла "чё мы ваще пришли на эту страницу"
useAutoPay(); useAutoPay();
@ -172,168 +176,192 @@ export default function Payment() {
px: isTablet ? (upMd ? "40px" : "18px") : "20px", px: isTablet ? (upMd ? "40px" : "18px") : "20px",
}} }}
> >
<Box {/* Показываем состояние загрузки во время авторизации */}
sx={{ {isProcessing && (
mt: "20px", <Box
mb: "40px", sx={{
display: "flex", display: "flex",
gap: "10px", flexDirection: "column",
}} alignItems: "center",
> justifyContent: "center",
{window.history.length > 1 && ( minHeight: "400px",
<IconButton gap: 2,
onClick={handleCustomBackNavigation} }}
sx={{ p: 0, height: "28px", width: "28px", color: "black" }} >
> <CircularProgress size={60} />
<ArrowBackIcon /> <Typography variant="h6" color="text.secondary">
</IconButton> Идёт авторизация...
)} </Typography>
<Typography variant="h4">Способ оплаты</Typography> </Box>
</Box>
{!upMd && (
<Typography variant="body2" mb="30px">
Выберите способ оплаты
</Typography>
)} )}
<Box
sx={{
backgroundColor: upMd ? "white" : undefined,
display: "flex",
flexDirection: upMd ? "row" : "column",
borderRadius: "12px",
boxShadow: upMd ? cardShadow : undefined,
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
p: upMd ? "20px" : undefined,
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: "40px",
}}
>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: upSm ? "row" : "column",
flexWrap: "wrap",
gap: upMd ? "14px" : "20px",
alignContent: "start",
}}
>
{paymentMethods.map(({ name, label, image, unpopular = false }) => (
<PaymentMethodCard
isSelected={selectedPaymentMethod === name}
key={name}
label={label}
image={image}
onClick={() => {
setSelectedPaymentMethod(name);
}}
unpopular={false}
/>
))}
<PaymentMethodCard
isSelected={selectedPaymentMethod === "rspay"}
label={"Расчётный счёт"}
image={rsPayLogo}
onClick={async () => {
setSelectedPaymentMethod("rspay");
}}
unpopular={false}
/>
</Box>
<CollapsiblePromocodeField
fieldValue={promocodeField}
onFieldChange={setPromocodeField}
onPromocodeApply={handleApplyPromocode}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.gray.dark,
width: upMd ? "31.5%" : undefined,
p: upMd ? "20px" : undefined,
pl: upMd ? "33px" : undefined,
mt: upMd ? undefined : "30px",
borderLeft: upMd
? `1px solid ${theme.palette.gray.main}`
: undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
maxWidth: "85%",
}}
>
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>}
<Typography mb="20px">К оплате</Typography>
{
siteReadyPayCart?.[userId] && notEnoughMoneyAmount > 0 ?
<Typography
sx={{
fontWeight: 500,
fontSize: "20px",
lineHeight: "48px",
mb: "28px",
}}
>
{currencyFormatter.format(
Number(bigDecimal.divide(bigDecimal.floor(paymentValue), 100))
)}
</Typography>
:
<InputTextfield
TextfieldProps={{
placeholder: "К оплате",
value: paymentValueField,
type: "number",
}}
onChange={(e) => {
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" }}
/>
}
</Box> {/* Основной контент страницы */}
<Button {!isProcessing && (
variant="pena-outlined-light" <>
disabled={!isFinite(paymentValue)} <Box
onClick={handlePaymentClick}
sx={{ sx={{
mt: "auto", mt: "20px",
color: "black", mb: "40px",
border: `1px solid ${theme.palette.purple.main}`, display: "flex",
"&:hover": { gap: "10px",
color: "white",
},
"&:active": {
color: "white",
},
}} }}
> >
Оплатить {window.history.length > 1 && (
</Button> <IconButton
</Box> onClick={handleCustomBackNavigation}
</Box> sx={{ p: 0, height: "28px", width: "28px", color: "black" }}
<WarnModal open={warnModalOpen} setOpen={setWarnModalOpen} /> >
<SorryModal open={sorryModalOpen} setOpen={setSorryModalOpen} /> <ArrowBackIcon />
</IconButton>
)}
<Typography variant="h4">Способ оплаты</Typography>
</Box>
{!upMd && (
<Typography variant="body2" mb="30px">
Выберите способ оплаты
</Typography>
)}
<Box
sx={{
backgroundColor: upMd ? "white" : undefined,
display: "flex",
flexDirection: upMd ? "row" : "column",
borderRadius: "12px",
boxShadow: upMd ? cardShadow : undefined,
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
p: upMd ? "20px" : undefined,
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: "40px",
}}
>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: upSm ? "row" : "column",
flexWrap: "wrap",
gap: upMd ? "14px" : "20px",
alignContent: "start",
}}
>
{paymentMethods.map(({ name, label, image, unpopular = false }) => (
<PaymentMethodCard
isSelected={selectedPaymentMethod === name}
key={name}
label={label}
image={image}
onClick={() => {
setSelectedPaymentMethod(name);
}}
unpopular={false}
/>
))}
<PaymentMethodCard
isSelected={selectedPaymentMethod === "rspay"}
label={"Расчётный счёт"}
image={rsPayLogo}
onClick={async () => {
setSelectedPaymentMethod("rspay");
}}
unpopular={false}
/>
</Box>
<CollapsiblePromocodeField
fieldValue={promocodeField}
onFieldChange={setPromocodeField}
onPromocodeApply={handleApplyPromocode}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.gray.dark,
width: upMd ? "31.5%" : undefined,
p: upMd ? "20px" : undefined,
pl: upMd ? "33px" : undefined,
mt: upMd ? undefined : "30px",
borderLeft: upMd
? `1px solid ${theme.palette.gray.main}`
: undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
maxWidth: "85%",
}}
>
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>}
<Typography mb="20px">К оплате</Typography>
{
siteReadyPayCart?.[userId] && notEnoughMoneyAmount > 0 ?
<Typography
sx={{
fontWeight: 500,
fontSize: "20px",
lineHeight: "48px",
mb: "28px",
}}
>
{currencyFormatter.format(
Number(bigDecimal.divide(bigDecimal.floor(paymentValue), 100))
)}
</Typography>
:
<InputTextfield
TextfieldProps={{
placeholder: "К оплате",
value: paymentValueField,
type: "number",
}}
onChange={(e) => {
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" }}
/>
}
</Box>
<Button
variant="pena-outlined-light"
disabled={!isFinite(paymentValue)}
onClick={handlePaymentClick}
sx={{
mt: "auto",
color: "black",
border: `1px solid ${theme.palette.purple.main}`,
"&:hover": {
color: "white",
},
"&:active": {
color: "white",
},
}}
>
Оплатить
</Button>
</Box>
</Box>
<WarnModal open={warnModalOpen} setOpen={setWarnModalOpen} />
<SorryModal open={sorryModalOpen} setOpen={setSorryModalOpen} />
</>
)}
</SectionWrapper> </SectionWrapper>
); );
} }

@ -3,21 +3,94 @@ import { logout } from '@root/api/auth';
import { setNotEnoughMoneyAmount } from '@root/stores/allTypesOfPurchases'; import { setNotEnoughMoneyAmount } from '@root/stores/allTypesOfPurchases';
import { clearCustomTariffs } from '@root/stores/customTariffs'; import { clearCustomTariffs } from '@root/stores/customTariffs';
import { clearTickets } from '@root/stores/tickets'; import { clearTickets } from '@root/stores/tickets';
import { clearUserData, useUserStore } from '@root/stores/user'; import { clearUserData, setUserId, useUserStore } from '@root/stores/user';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
interface QuizAuthParams {
action?: string;
dif?: string;
data?: string;
userid?: string;
wayback?: string;
}
export const useReauthorization = () => { export const useReauthorization = () => {
const userId = useUserStore(store => store.userId) const userId = useUserStore(store => store.userId);
const user = useUserStore(store => store.user);
const { search } = useLocation(); const { search } = useLocation();
const navigate = useNavigate();
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => { useEffect(() => {
// Этот эффект сработает при каждом изменении query-параметров // Этот эффект сработает при каждом изменении query-параметров
const params = new URLSearchParams(search); const params = new URLSearchParams(search);
// Обработка старых параметров (userid, sec)
const URLuserId = params.get('userid'); const URLuserId = params.get('userid');
const URLtoken = params.get('sec'); 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()) {
clearAuthToken();
clearUserData();
clearCustomTariffs();
clearTickets();
setNotEnoughMoneyAmount(0);
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) { if (URLuserId !== userId && URLtoken) {
// Если есть токен в URL, устанавливаем его // Если есть токен в URL, устанавливаем его
// Очищаем данные только если токен действительно изменился // Очищаем данные только если токен действительно изменился
@ -31,5 +104,9 @@ export const useReauthorization = () => {
} }
setAuthToken(URLtoken); setAuthToken(URLtoken);
} }
}, [search]); }, [search, userId, user?._id, navigate, isProcessing]);
return {
isProcessing,
};
}; };